mirror of
https://github.com/aykhans/slash-e.git
synced 2025-07-06 21:22:36 +00:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
a379614cd9 | |||
c18bbfd0bb | |||
d798b2c5fb | |||
4e3ca8ceb4 | |||
96a68ab117 | |||
0eea0a92db | |||
4a47010608 | |||
fa504a88e5 | |||
de51e1a8d3 | |||
49cc1e9755 | |||
ce5c4b65d3 | |||
cee6c7c401 | |||
b6839d2b7d | |||
0ebf03eb9b | |||
21eab35e45 | |||
fd1168e1dc | |||
5ee32d2e78 | |||
2db9c1e850 | |||
953ec3dbc0 | |||
fc28473aee | |||
c42c543618 | |||
72106d13de | |||
6bbf2df8e0 |
@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: stevenlgtm
|
username: yourselfhosted
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@ -41,4 +41,4 @@ jobs:
|
|||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: stevenlgtm/slash:latest, stevenlgtm/slash:${{ env.VERSION }}
|
tags: yourselfhosted/slash:latest, yourselfhosted/slash:${{ env.VERSION }}
|
||||||
|
@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: stevenlgtm
|
username: yourselfhosted
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@ -34,4 +34,4 @@ jobs:
|
|||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: stevenlgtm/slash:test
|
tags: yourselfhosted/slash:test
|
||||||
|
12
README.md
12
README.md
@ -2,27 +2,27 @@
|
|||||||
|
|
||||||
<img align="right" src="./resources/logo.png" height="64px" alt="logo">
|
<img align="right" src="./resources/logo.png" height="64px" alt="logo">
|
||||||
|
|
||||||
`Slash` is a bookmarking and link shortening service that enables easy saving and sharing of links. It allows you to store, categorize, and share links with custom short URLs. You can search, filter, and access your saved links from any device. It also supports team sharing of link libraries for easy collaboration.
|
**Slash** is an open source, self-hosted bookmarks and link sharing platform. It allows you to organize your links with tags, and share them using custom shortened URLs. Slash also supports team sharing of link libraries for easy collaboration.
|
||||||
|
|
||||||
Try it out on <a href="https://slash.stevenlgtm.com">Live Demo</a>.
|
Try it out on <a href="https://slash.yourselfhosted.com">Live Demo</a>.
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://discord.gg/QZqUuUAhDV"><img alt="Discord" src="https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5" /></a>
|
<a href="https://discord.gg/QZqUuUAhDV"><img alt="Discord" src="https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5" /></a>
|
||||||
<a href="https://hub.docker.com/r/stevenlgtm/slash"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/stevenlgtm/slash.svg" /></a>
|
<a href="https://hub.docker.com/r/yourselfhosted/slash"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/yourselfhosted/slash.svg" /></a>
|
||||||
<a href="https://github.com/boojack/slash/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/boojack/slash?logo=github" /></a>
|
<a href="https://github.com/boojack/slash/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/boojack/slash?logo=github" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Create customizable `/s/` short links for any URL.
|
- Create customizable `/s/` short links for any URL.
|
||||||
- Share short links privately or with others.
|
- Share short links privately or with teammates.
|
||||||
- View analytics on short link traffic and sources.
|
- View analytics on link traffic and sources.
|
||||||
- Open source self-hosted solution.
|
- Open source self-hosted solution.
|
||||||
|
|
||||||
## Deploy with Docker in seconds
|
## Deploy with Docker in seconds
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d --name slash -p 5231:5231 -v ~/.slash/:/var/opt/slash stevenlgtm/slash:latest
|
docker run -d --name slash -p 5231:5231 -v ~/.slash/:/var/opt/slash yourselfhosted/slash:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Learn more in [Self-hosting Slash with Docker](https://github.com/boojack/slash/blob/main/docs/install.md).
|
Learn more in [Self-hosting Slash with Docker](https://github.com/boojack/slash/blob/main/docs/install.md).
|
||||||
|
@ -23,14 +23,11 @@ const (
|
|||||||
apiTokenDuration = 2 * time.Hour
|
apiTokenDuration = 2 * time.Hour
|
||||||
accessTokenDuration = 24 * time.Hour
|
accessTokenDuration = 24 * time.Hour
|
||||||
refreshTokenDuration = 7 * 24 * time.Hour
|
refreshTokenDuration = 7 * 24 * time.Hour
|
||||||
// RefreshThresholdDuration is the threshold duration for refreshing token.
|
|
||||||
RefreshThresholdDuration = 1 * time.Hour
|
|
||||||
|
|
||||||
// CookieExpDuration expires slightly earlier than the jwt expiration. Client would be logged out if the user
|
// CookieExpDuration expires slightly earlier than the jwt expiration. Client would be logged out if the user
|
||||||
// cookie expires, thus the client would always logout first before attempting to make a request with the expired jwt.
|
// cookie expires, thus the client would always logout first before attempting to make a request with the expired jwt.
|
||||||
// Suppose we have a valid refresh token, we will refresh the token in 2 cases:
|
// Suppose we have a valid refresh token, we will refresh the token in the following cases:
|
||||||
// 1. The access token is about to expire in <<refreshThresholdDuration>>
|
// 1. The access token has already expired, we refresh the token so that the ongoing request can pass through.
|
||||||
// 2. The access token has already expired, we refresh the token so that the ongoing request can pass through.
|
|
||||||
CookieExpDuration = refreshTokenDuration - 1*time.Minute
|
CookieExpDuration = refreshTokenDuration - 1*time.Minute
|
||||||
// AccessTokenCookieName is the cookie name of access token.
|
// AccessTokenCookieName is the cookie name of access token.
|
||||||
AccessTokenCookieName = "slash.access-token"
|
AccessTokenCookieName = "slash.access-token"
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/boojack/slash/api/v1/auth"
|
"github.com/boojack/slash/api/v1/auth"
|
||||||
"github.com/boojack/slash/internal/util"
|
"github.com/boojack/slash/internal/util"
|
||||||
@ -105,12 +104,12 @@ func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) e
|
|||||||
return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
|
return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
|
||||||
})
|
})
|
||||||
|
|
||||||
generateToken := time.Until(claims.ExpiresAt.Time) < auth.RefreshThresholdDuration
|
generateToken := false
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var ve *jwt.ValidationError
|
var ve *jwt.ValidationError
|
||||||
if errors.As(err, &ve) {
|
if errors.As(err, &ve) {
|
||||||
// If expiration error is the only error, we will clear the err
|
// If expiration error is the only error, we will ignore the err
|
||||||
// and generate new access token and refresh token
|
// and generate new access token and refresh token.
|
||||||
if ve.Errors == jwt.ValidationErrorExpired {
|
if ve.Errors == jwt.ValidationErrorExpired {
|
||||||
generateToken = true
|
generateToken = true
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ var (
|
|||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "slash",
|
Use: "slash",
|
||||||
Short: `A bookmarking and url shortener, save and share your links very easily.`,
|
Short: `An open source, self-hosted bookmarks and link sharing platform.`,
|
||||||
Run: func(_cmd *cobra.Command, _args []string) {
|
Run: func(_cmd *cobra.Command, _args []string) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
db := db.NewDB(profile)
|
db := db.NewDB(profile)
|
||||||
|
@ -11,7 +11,7 @@ The only requirement is a server with Docker installed.
|
|||||||
To deploy Slash using docker run, just one command is needed:
|
To deploy Slash using docker run, just one command is needed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d --name slash --publish 5231:5231 --volume ~/.slash/:/var/opt/slash stevenlgtm/slash:latest
|
docker run -d --name slash --publish 5231:5231 --volume ~/.slash/:/var/opt/slash yourselfhosted/slash:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start Slash in the background and expose it on port `5231`. Data is stored in `~/.slash/`. You can customize the port and data directory.
|
This will start Slash in the background and expose it on port `5231`. Data is stored in `~/.slash/`. You can customize the port and data directory.
|
||||||
@ -33,7 +33,7 @@ cp -r ~/.slash/slash_prod.db ~/.slash/slash_prod.db.bak
|
|||||||
Then pull the latest image:
|
Then pull the latest image:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull stevenlgtm/slash:latest
|
docker pull yourselfhosted/slash:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, restart Slash by following the steps in [Docker Run](#docker-run).
|
Finally, restart Slash by following the steps in [Docker Run](#docker-run).
|
||||||
|
@ -10,7 +10,7 @@ VALUES
|
|||||||
(
|
(
|
||||||
101,
|
101,
|
||||||
'ADMIN',
|
'ADMIN',
|
||||||
'slash@stevenlgtm.com',
|
'slash@yourselfhosted.com',
|
||||||
'Slasher',
|
'Slasher',
|
||||||
'$2a$10$H8HBWGcG/hoePhFy5SiNKOHxMD6omIpyEEWbl/fIorFC814bXW.Ua'
|
'$2a$10$H8HBWGcG/hoePhFy5SiNKOHxMD6omIpyEEWbl/fIorFC814bXW.Ua'
|
||||||
);
|
);
|
||||||
|
@ -22,6 +22,7 @@ INSERT INTO
|
|||||||
`name`,
|
`name`,
|
||||||
`link`,
|
`link`,
|
||||||
`visibility`,
|
`visibility`,
|
||||||
|
`tag`,
|
||||||
`og_metadata`
|
`og_metadata`
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
@ -31,6 +32,7 @@ VALUES
|
|||||||
'ai-infra',
|
'ai-infra',
|
||||||
'https://star-history.com/blog/open-source-ai-infra-projects',
|
'https://star-history.com/blog/open-source-ai-infra-projects',
|
||||||
'PUBLIC',
|
'PUBLIC',
|
||||||
|
'star-history ai',
|
||||||
'{"title":"Open Source AI Infra for Your Next Project","description":"Some open-source infra projects that can be directly used for your next project. 💡","image":"https://star-history.com/blog/assets/open-source-ai-infra-projects/banner.webp"}'
|
'{"title":"Open Source AI Infra for Your Next Project","description":"Some open-source infra projects that can be directly used for your next project. 💡","image":"https://star-history.com/blog/assets/open-source-ai-infra-projects/banner.webp"}'
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -41,6 +43,7 @@ INSERT INTO
|
|||||||
`name`,
|
`name`,
|
||||||
`link`,
|
`link`,
|
||||||
`visibility`,
|
`visibility`,
|
||||||
|
`tag`,
|
||||||
`og_metadata`
|
`og_metadata`
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
@ -50,6 +53,7 @@ VALUES
|
|||||||
'schema-change',
|
'schema-change',
|
||||||
'https://www.bytebase.com/blog/how-to-handle-database-schema-change/#what-is-a-database-schema-change',
|
'https://www.bytebase.com/blog/how-to-handle-database-schema-change/#what-is-a-database-schema-change',
|
||||||
'PUBLIC',
|
'PUBLIC',
|
||||||
|
'database article👍',
|
||||||
'{"title":"How to Handle Database Migration / Schema Change?","description":"A database schema is the structure of a database, which describes the relationships between the different tables and fields in the database. A database schema change, also known as schema migration, or simply migration refers to any alteration to this structure, such as adding a new table, modifying the data type of a field, or changing the relationships between tables.","image":"https://www.bytebase.com/_next/image/?url=%2Fcontent%2Fblog%2Fhow-to-handle-database-schema-change%2Fchange.webp\u0026w=2048\u0026q=75"}'
|
'{"title":"How to Handle Database Migration / Schema Change?","description":"A database schema is the structure of a database, which describes the relationships between the different tables and fields in the database. A database schema change, also known as schema migration, or simply migration refers to any alteration to this structure, such as adding a new table, modifying the data type of a field, or changing the relationships between tables.","image":"https://www.bytebase.com/_next/image/?url=%2Fcontent%2Fblog%2Fhow-to-handle-database-schema-change%2Fchange.webp\u0026w=2048\u0026q=75"}'
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -59,6 +63,7 @@ INSERT INTO
|
|||||||
`creator_id`,
|
`creator_id`,
|
||||||
`name`,
|
`name`,
|
||||||
`link`,
|
`link`,
|
||||||
|
`tag`,
|
||||||
`visibility`
|
`visibility`
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
@ -67,6 +72,7 @@ VALUES
|
|||||||
101,
|
101,
|
||||||
'sqlchat',
|
'sqlchat',
|
||||||
'https://www.sqlchat.ai',
|
'https://www.sqlchat.ai',
|
||||||
|
'ai chatbot sql',
|
||||||
'WORKSPACE'
|
'WORKSPACE'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -33,8 +33,8 @@
|
|||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.2.0",
|
"@types/react-dom": "^18.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||||
"@typescript-eslint/parser": "^5.6.0",
|
"@typescript-eslint/parser": "^6.2.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"eslint": "^8.4.1",
|
"eslint": "^8.4.1",
|
||||||
|
265
web/pnpm-lock.yaml
generated
265
web/pnpm-lock.yaml
generated
@ -80,11 +80,11 @@ devDependencies:
|
|||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.5
|
version: 18.2.5
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: ^5.6.0
|
specifier: ^6.2.0
|
||||||
version: 5.6.0(@typescript-eslint/parser@5.6.0)(eslint@8.4.1)(typescript@5.0.4)
|
version: 6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.4.1)(typescript@5.0.4)
|
||||||
'@typescript-eslint/parser':
|
'@typescript-eslint/parser':
|
||||||
specifier: ^5.6.0
|
specifier: ^6.2.0
|
||||||
version: 5.6.0(eslint@8.4.1)(typescript@5.0.4)
|
version: 6.2.0(eslint@8.4.1)(typescript@5.0.4)
|
||||||
'@vitejs/plugin-react-swc':
|
'@vitejs/plugin-react-swc':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0(vite@4.0.0)
|
version: 3.0.0(vite@4.0.0)
|
||||||
@ -486,6 +486,21 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@eslint-community/eslint-utils@4.4.0(eslint@8.4.1):
|
||||||
|
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.4.1
|
||||||
|
eslint-visitor-keys: 3.4.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@eslint-community/regexpp@4.6.2:
|
||||||
|
resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==}
|
||||||
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@eslint/eslintrc@1.4.1:
|
/@eslint/eslintrc@1.4.1:
|
||||||
resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==}
|
resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
@ -919,113 +934,143 @@ packages:
|
|||||||
/@types/scheduler@0.16.3:
|
/@types/scheduler@0.16.3:
|
||||||
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
|
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
|
||||||
|
|
||||||
|
/@types/semver@7.5.0:
|
||||||
|
resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/use-sync-external-store@0.0.3:
|
/@types/use-sync-external-store@0.0.3:
|
||||||
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
|
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@typescript-eslint/eslint-plugin@5.6.0(@typescript-eslint/parser@5.6.0)(eslint@8.4.1)(typescript@5.0.4):
|
/@typescript-eslint/eslint-plugin@6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.4.1)(typescript@5.0.4):
|
||||||
resolution: {integrity: sha512-MIbeMy5qfLqtgs1hWd088k1hOuRsN9JrHUPwVVKCD99EOUqScd7SrwoZl4Gso05EAP9w1kvLWUVGJOVpRPkDPA==}
|
resolution: {integrity: sha512-rClGrMuyS/3j0ETa1Ui7s6GkLhfZGKZL3ZrChLeAiACBE/tRc1wq8SNZESUuluxhLj9FkUefRs2l6bCIArWBiQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^16.0.0 || >=18.0.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@typescript-eslint/parser': ^5.0.0
|
'@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
|
||||||
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
|
eslint: ^7.0.0 || ^8.0.0
|
||||||
typescript: '*'
|
typescript: '*'
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/experimental-utils': 5.6.0(eslint@8.4.1)(typescript@5.0.4)
|
'@eslint-community/regexpp': 4.6.2
|
||||||
'@typescript-eslint/parser': 5.6.0(eslint@8.4.1)(typescript@5.0.4)
|
'@typescript-eslint/parser': 6.2.0(eslint@8.4.1)(typescript@5.0.4)
|
||||||
'@typescript-eslint/scope-manager': 5.6.0
|
'@typescript-eslint/scope-manager': 6.2.0
|
||||||
|
'@typescript-eslint/type-utils': 6.2.0(eslint@8.4.1)(typescript@5.0.4)
|
||||||
|
'@typescript-eslint/utils': 6.2.0(eslint@8.4.1)(typescript@5.0.4)
|
||||||
|
'@typescript-eslint/visitor-keys': 6.2.0
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
eslint: 8.4.1
|
eslint: 8.4.1
|
||||||
functional-red-black-tree: 1.0.1
|
graphemer: 1.4.0
|
||||||
ignore: 5.2.4
|
ignore: 5.2.4
|
||||||
regexpp: 3.2.0
|
natural-compare: 1.4.0
|
||||||
semver: 7.5.1
|
natural-compare-lite: 1.4.0
|
||||||
tsutils: 3.21.0(typescript@5.0.4)
|
semver: 7.5.4
|
||||||
|
ts-api-utils: 1.0.1(typescript@5.0.4)
|
||||||
typescript: 5.0.4
|
typescript: 5.0.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@typescript-eslint/experimental-utils@5.6.0(eslint@8.4.1)(typescript@5.0.4):
|
/@typescript-eslint/parser@6.2.0(eslint@8.4.1)(typescript@5.0.4):
|
||||||
resolution: {integrity: sha512-VDoRf3Qj7+W3sS/ZBXZh3LBzp0snDLEgvp6qj0vOAIiAPM07bd5ojQ3CTzF/QFl5AKh7Bh1ycgj6lFBJHUt/DA==}
|
resolution: {integrity: sha512-igVYOqtiK/UsvKAmmloQAruAdUHihsOCvplJpplPZ+3h4aDkC/UKZZNKgB6h93ayuYLuEymU3h8nF1xMRbh37g==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^16.0.0 || >=18.0.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '*'
|
eslint: ^7.0.0 || ^8.0.0
|
||||||
|
typescript: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/json-schema': 7.0.12
|
'@typescript-eslint/scope-manager': 6.2.0
|
||||||
'@typescript-eslint/scope-manager': 5.6.0
|
'@typescript-eslint/types': 6.2.0
|
||||||
'@typescript-eslint/types': 5.6.0
|
'@typescript-eslint/typescript-estree': 6.2.0(typescript@5.0.4)
|
||||||
'@typescript-eslint/typescript-estree': 5.6.0(typescript@5.0.4)
|
'@typescript-eslint/visitor-keys': 6.2.0
|
||||||
|
debug: 4.3.4
|
||||||
eslint: 8.4.1
|
eslint: 8.4.1
|
||||||
eslint-scope: 5.1.1
|
typescript: 5.0.4
|
||||||
eslint-utils: 3.0.0(eslint@8.4.1)
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@typescript-eslint/scope-manager@6.2.0:
|
||||||
|
resolution: {integrity: sha512-1ZMNVgm5nnHURU8ZSJ3snsHzpFeNK84rdZjluEVBGNu7jDymfqceB3kdIZ6A4xCfEFFhRIB6rF8q/JIqJd2R0Q==}
|
||||||
|
engines: {node: ^16.0.0 || >=18.0.0}
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/types': 6.2.0
|
||||||
|
'@typescript-eslint/visitor-keys': 6.2.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@typescript-eslint/type-utils@6.2.0(eslint@8.4.1)(typescript@5.0.4):
|
||||||
|
resolution: {integrity: sha512-DnGZuNU2JN3AYwddYIqrVkYW0uUQdv0AY+kz2M25euVNlujcN2u+rJgfJsBFlUEzBB6OQkUqSZPyuTLf2bP5mw==}
|
||||||
|
engines: {node: ^16.0.0 || >=18.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^7.0.0 || ^8.0.0
|
||||||
|
typescript: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/typescript-estree': 6.2.0(typescript@5.0.4)
|
||||||
|
'@typescript-eslint/utils': 6.2.0(eslint@8.4.1)(typescript@5.0.4)
|
||||||
|
debug: 4.3.4
|
||||||
|
eslint: 8.4.1
|
||||||
|
ts-api-utils: 1.0.1(typescript@5.0.4)
|
||||||
|
typescript: 5.0.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@typescript-eslint/types@6.2.0:
|
||||||
|
resolution: {integrity: sha512-1nRRaDlp/XYJQLvkQJG5F3uBTno5SHPT7XVcJ5n1/k2WfNI28nJsvLakxwZRNY5spuatEKO7d5nZWsQpkqXwBA==}
|
||||||
|
engines: {node: ^16.0.0 || >=18.0.0}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@typescript-eslint/typescript-estree@6.2.0(typescript@5.0.4):
|
||||||
|
resolution: {integrity: sha512-Mts6+3HQMSM+LZCglsc2yMIny37IhUgp1Qe8yJUYVyO6rHP7/vN0vajKu3JvHCBIy8TSiKddJ/Zwu80jhnGj1w==}
|
||||||
|
engines: {node: ^16.0.0 || >=18.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/types': 6.2.0
|
||||||
|
'@typescript-eslint/visitor-keys': 6.2.0
|
||||||
|
debug: 4.3.4
|
||||||
|
globby: 11.1.0
|
||||||
|
is-glob: 4.0.3
|
||||||
|
semver: 7.5.4
|
||||||
|
ts-api-utils: 1.0.1(typescript@5.0.4)
|
||||||
|
typescript: 5.0.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@typescript-eslint/utils@6.2.0(eslint@8.4.1)(typescript@5.0.4):
|
||||||
|
resolution: {integrity: sha512-RCFrC1lXiX1qEZN8LmLrxYRhOkElEsPKTVSNout8DMzf8PeWoQG7Rxz2SadpJa3VSh5oYKGwt7j7X/VRg+Y3OQ==}
|
||||||
|
engines: {node: ^16.0.0 || >=18.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^7.0.0 || ^8.0.0
|
||||||
|
dependencies:
|
||||||
|
'@eslint-community/eslint-utils': 4.4.0(eslint@8.4.1)
|
||||||
|
'@types/json-schema': 7.0.12
|
||||||
|
'@types/semver': 7.5.0
|
||||||
|
'@typescript-eslint/scope-manager': 6.2.0
|
||||||
|
'@typescript-eslint/types': 6.2.0
|
||||||
|
'@typescript-eslint/typescript-estree': 6.2.0(typescript@5.0.4)
|
||||||
|
eslint: 8.4.1
|
||||||
|
semver: 7.5.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@typescript-eslint/parser@5.6.0(eslint@8.4.1)(typescript@5.0.4):
|
/@typescript-eslint/visitor-keys@6.2.0:
|
||||||
resolution: {integrity: sha512-YVK49NgdUPQ8SpCZaOpiq1kLkYRPMv9U5gcMrywzI8brtwZjr/tG3sZpuHyODt76W/A0SufNjYt9ZOgrC4tLIQ==}
|
resolution: {integrity: sha512-QbaYUQVKKo9bgCzpjz45llCfwakyoxHetIy8CAvYCtd16Zu1KrpzNHofwF8kGkpPOxZB2o6kz+0nqH8ZkIzuoQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^16.0.0 || >=18.0.0}
|
||||||
peerDependencies:
|
|
||||||
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
|
|
||||||
typescript: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
typescript:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 5.6.0
|
'@typescript-eslint/types': 6.2.0
|
||||||
'@typescript-eslint/types': 5.6.0
|
|
||||||
'@typescript-eslint/typescript-estree': 5.6.0(typescript@5.0.4)
|
|
||||||
debug: 4.3.4
|
|
||||||
eslint: 8.4.1
|
|
||||||
typescript: 5.0.4
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@typescript-eslint/scope-manager@5.6.0:
|
|
||||||
resolution: {integrity: sha512-1U1G77Hw2jsGWVsO2w6eVCbOg0HZ5WxL/cozVSTfqnL/eB9muhb8THsP0G3w+BB5xAHv9KptwdfYFAUfzcIh4A==}
|
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/types': 5.6.0
|
|
||||||
'@typescript-eslint/visitor-keys': 5.6.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@typescript-eslint/types@5.6.0:
|
|
||||||
resolution: {integrity: sha512-OIZffked7mXv4mXzWU5MgAEbCf9ecNJBKi+Si6/I9PpTaj+cf2x58h2oHW5/P/yTnPkKaayfjhLvx+crnl5ubA==}
|
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@typescript-eslint/typescript-estree@5.6.0(typescript@5.0.4):
|
|
||||||
resolution: {integrity: sha512-92vK5tQaE81rK7fOmuWMrSQtK1IMonESR+RJR2Tlc7w4o0MeEdjgidY/uO2Gobh7z4Q1hhS94Cr7r021fMVEeA==}
|
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
|
||||||
peerDependencies:
|
|
||||||
typescript: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
typescript:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/types': 5.6.0
|
|
||||||
'@typescript-eslint/visitor-keys': 5.6.0
|
|
||||||
debug: 4.3.4
|
|
||||||
globby: 11.1.0
|
|
||||||
is-glob: 4.0.3
|
|
||||||
semver: 7.5.1
|
|
||||||
tsutils: 3.21.0(typescript@5.0.4)
|
|
||||||
typescript: 5.0.4
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@typescript-eslint/visitor-keys@5.6.0:
|
|
||||||
resolution: {integrity: sha512-1p7hDp5cpRFUyE3+lvA74egs+RWSgumrBpzBCDzfTFv0aQ7lIeay80yU0hIxgAhwQ6PcasW35kaOCyDOv6O/Ng==}
|
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/types': 5.6.0
|
|
||||||
eslint-visitor-keys: 3.4.1
|
eslint-visitor-keys: 3.4.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@ -1586,14 +1631,6 @@ packages:
|
|||||||
string.prototype.matchall: 4.0.8
|
string.prototype.matchall: 4.0.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/eslint-scope@5.1.1:
|
|
||||||
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
|
|
||||||
engines: {node: '>=8.0.0'}
|
|
||||||
dependencies:
|
|
||||||
esrecurse: 4.3.0
|
|
||||||
estraverse: 4.3.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/eslint-scope@7.2.0:
|
/eslint-scope@7.2.0:
|
||||||
resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==}
|
resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
@ -1692,11 +1729,6 @@ packages:
|
|||||||
estraverse: 5.3.0
|
estraverse: 5.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/estraverse@4.3.0:
|
|
||||||
resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==}
|
|
||||||
engines: {node: '>=4.0'}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/estraverse@5.3.0:
|
/estraverse@5.3.0:
|
||||||
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@ -1918,6 +1950,10 @@ packages:
|
|||||||
get-intrinsic: 1.2.1
|
get-intrinsic: 1.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/graphemer@1.4.0:
|
||||||
|
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/has-bigints@1.0.2:
|
/has-bigints@1.0.2:
|
||||||
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
|
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -2274,6 +2310,10 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
/natural-compare-lite@1.4.0:
|
||||||
|
resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/natural-compare@1.4.0:
|
/natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -2770,6 +2810,14 @@ packages:
|
|||||||
lru-cache: 6.0.0
|
lru-cache: 6.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/semver@7.5.4:
|
||||||
|
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
lru-cache: 6.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/shebang-command@2.0.0:
|
/shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -2954,24 +3002,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
|
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/ts-api-utils@1.0.1(typescript@5.0.4):
|
||||||
|
resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==}
|
||||||
|
engines: {node: '>=16.13.0'}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.2.0'
|
||||||
|
dependencies:
|
||||||
|
typescript: 5.0.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ts-interface-checker@0.1.13:
|
/ts-interface-checker@0.1.13:
|
||||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/tslib@1.14.1:
|
|
||||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/tsutils@3.21.0(typescript@5.0.4):
|
|
||||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
peerDependencies:
|
|
||||||
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
|
|
||||||
dependencies:
|
|
||||||
tslib: 1.14.1
|
|
||||||
typescript: 5.0.4
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/type-check@0.4.0:
|
/type-check@0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import { globalService } from "./services";
|
import { globalService } from "./services";
|
||||||
import useUserStore from "./stores/v1/user";
|
import useUserStore from "./stores/v1/user";
|
||||||
|
import DemoBanner from "./components/DemoBanner";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@ -27,7 +28,14 @@ function App() {
|
|||||||
initialState();
|
initialState();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <>{!loading && <Outlet />}</>;
|
return !loading ? (
|
||||||
|
<>
|
||||||
|
<DemoBanner />
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -19,7 +19,7 @@ const AboutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="max-w-full w-80 sm:w-96">
|
<div className="max-w-full w-80 sm:w-96">
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">Slash</span>: A bookmarking and url shortener, save and share your links very easily.
|
<span className="font-medium">Slash</span>: An open source, self-hosted bookmarks and link sharing platform.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<span className="mr-2">See more in</span>
|
<span className="mr-2">See more in</span>
|
||||||
|
@ -37,7 +37,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [showDescriptionAndTag, setShowDescriptionAndTag] = useState<boolean>(false);
|
const [showAdditionalFields, setShowAdditionalFields] = useState<boolean>(false);
|
||||||
const [showOpenGraphMetadata, setShowOpenGraphMetadata] = useState<boolean>(false);
|
const [showOpenGraphMetadata, setShowOpenGraphMetadata] = useState<boolean>(false);
|
||||||
const [tag, setTag] = useState<string>("");
|
const [tag, setTag] = useState<string>("");
|
||||||
const requestState = useLoading(false);
|
const requestState = useLoading(false);
|
||||||
@ -185,9 +185,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto overflow-x-hidden">
|
<div className="overflow-y-auto overflow-x-hidden">
|
||||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||||
<span className="mb-2">
|
<span className="mb-2">Name</span>
|
||||||
Name <span className="text-red-600">*</span>
|
|
||||||
</span>
|
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@ -199,21 +197,21 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||||
<span className="mb-2">
|
<span className="mb-2">Destination URL</span>
|
||||||
Destination URL <span className="text-red-600">*</span>
|
|
||||||
</span>
|
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. https://github.com/boojack/slash"
|
placeholder="https://github.com/boojack/slash"
|
||||||
value={state.shortcutCreate.link}
|
value={state.shortcutCreate.link}
|
||||||
onChange={handleLinkInputChange}
|
onChange={handleLinkInputChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||||
<span className="mb-2">
|
<span className="mb-2">Tags</span>
|
||||||
Visibility <span className="text-red-600">*</span>
|
<Input className="w-full" type="text" placeholder="github slash" value={tag} onChange={handleTagsInputChange} />
|
||||||
</span>
|
</div>
|
||||||
|
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||||
|
<span className="mb-2">Visibility</span>
|
||||||
<div className="w-full flex flex-row justify-start items-center text-base">
|
<div className="w-full flex flex-row justify-start items-center text-base">
|
||||||
<RadioGroup orientation="horizontal" value={state.shortcutCreate.visibility} onChange={handleVisibilityInputChange}>
|
<RadioGroup orientation="horizontal" value={state.shortcutCreate.visibility} onChange={handleVisibilityInputChange}>
|
||||||
{visibilities.map((visibility) => (
|
{visibilities.map((visibility) => (
|
||||||
@ -230,39 +228,28 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"w-full flex flex-row justify-between items-center px-2 py-1 cursor-pointer hover:bg-gray-100",
|
"w-full flex flex-row justify-between items-center px-2 py-1 cursor-pointer hover:bg-gray-100",
|
||||||
showDescriptionAndTag ? "bg-gray-100 border-b" : ""
|
showAdditionalFields ? "bg-gray-100 border-b" : ""
|
||||||
)}
|
)}
|
||||||
onClick={() => setShowDescriptionAndTag(!showDescriptionAndTag)}
|
onClick={() => setShowAdditionalFields(!showAdditionalFields)}
|
||||||
>
|
>
|
||||||
<span className="text-sm">Description and tags</span>
|
<span className="text-sm">Additional fields</span>
|
||||||
<button className="w-7 h-7 p-1 rounded-md">
|
<button className="w-7 h-7 p-1 rounded-md">
|
||||||
<Icon.ChevronDown className={classnames("w-4 h-auto text-gray-500", showDescriptionAndTag ? "transform rotate-180" : "")} />
|
<Icon.ChevronDown className={classnames("w-4 h-auto text-gray-500", showAdditionalFields ? "transform rotate-180" : "")} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{showDescriptionAndTag && (
|
{showAdditionalFields && (
|
||||||
<div className="w-full px-2 py-1">
|
<div className="w-full px-2 py-1">
|
||||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||||
<span className="mb-2 text-sm">Description</span>
|
<span className="mb-2 text-sm">Description</span>
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Something to describe the url"
|
placeholder="Github repo for slash"
|
||||||
size="sm"
|
size="sm"
|
||||||
value={state.shortcutCreate.description}
|
value={state.shortcutCreate.description}
|
||||||
onChange={handleDescriptionInputChange}
|
onChange={handleDescriptionInputChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
|
||||||
<span className="mb-2 text-sm">Tags</span>
|
|
||||||
<Input
|
|
||||||
className="w-full"
|
|
||||||
type="text"
|
|
||||||
placeholder="Separated by spaces"
|
|
||||||
size="sm"
|
|
||||||
value={tag}
|
|
||||||
onChange={handleTagsInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -288,7 +275,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="The image url"
|
placeholder="https://the.link.to/the/image.png"
|
||||||
size="sm"
|
size="sm"
|
||||||
value={state.shortcutCreate.openGraphMetadata.image}
|
value={state.shortcutCreate.openGraphMetadata.image}
|
||||||
onChange={handleOpenGraphMetadataImageChange}
|
onChange={handleOpenGraphMetadataImageChange}
|
||||||
@ -299,7 +286,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
className="w-full"
|
className="w-full"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Slash - A bookmarking and url shortener"
|
placeholder="Slash - An open source, self-hosted bookmarks and link sharing platform"
|
||||||
size="sm"
|
size="sm"
|
||||||
value={state.shortcutCreate.openGraphMetadata.title}
|
value={state.shortcutCreate.openGraphMetadata.title}
|
||||||
onChange={handleOpenGraphMetadataTitleChange}
|
onChange={handleOpenGraphMetadataTitleChange}
|
||||||
@ -309,7 +296,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<span className="mb-2 text-sm">Description</span>
|
<span className="mb-2 text-sm">Description</span>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="w-full"
|
className="w-full"
|
||||||
placeholder="A bookmarking and url shortener, save and share your links very easily."
|
placeholder="An open source, self-hosted bookmarks and link sharing platform."
|
||||||
size="sm"
|
size="sm"
|
||||||
maxRows={3}
|
maxRows={3}
|
||||||
value={state.shortcutCreate.openGraphMetadata.description}
|
value={state.shortcutCreate.openGraphMetadata.description}
|
||||||
|
@ -12,9 +12,9 @@ const DemoBanner: React.FC = () => {
|
|||||||
if (!shouldShow) return null;
|
if (!shouldShow) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="z-10 flex flex-row items-center justify-center w-full py-2 text-sm sm:text-lg font-medium dark:text-gray-300 bg-white dark:bg-zinc-700 shadow">
|
<div className="z-10 relative flex flex-row items-center justify-center w-full py-2 text-sm sm:text-lg font-medium dark:text-gray-300 bg-white dark:bg-zinc-700 shadow">
|
||||||
<div className="w-full max-w-4xl px-4 flex flex-row justify-between items-center gap-x-3">
|
<div className="w-full max-w-4xl px-4 flex flex-row justify-between items-center gap-x-3">
|
||||||
<span>✨A bookmarking and url shortener, save and share your links very easily.✨</span>
|
<span>✨Slash - An open source, self-hosted bookmarks and link sharing platform</span>
|
||||||
<a
|
<a
|
||||||
className="shadow flex flex-row justify-center items-center px-2 py-1 rounded-md text-sm sm:text-base text-white bg-blue-600 hover:bg-blue-700"
|
className="shadow flex flex-row justify-center items-center px-2 py-1 rounded-md text-sm sm:text-base text-white bg-blue-600 hover:bg-blue-700"
|
||||||
href="https://github.com/boojack/slash#deploy-with-docker-in-seconds"
|
href="https://github.com/boojack/slash#deploy-with-docker-in-seconds"
|
||||||
|
@ -21,7 +21,8 @@ const FilterView = () => {
|
|||||||
className="ml-2 px-2 py-1 flex flex-row justify-center items-center bg-gray-100 rounded-full text-gray-500 text-sm hover:line-through"
|
className="ml-2 px-2 py-1 flex flex-row justify-center items-center bg-gray-100 rounded-full text-gray-500 text-sm hover:line-through"
|
||||||
onClick={() => viewStore.setFilter({ tag: undefined })}
|
onClick={() => viewStore.setFilter({ tag: undefined })}
|
||||||
>
|
>
|
||||||
<Icon.Tag className="w-4 h-auto mr-1" />#{filter.tag}
|
<Icon.Tag className="w-4 h-auto mr-1" />
|
||||||
|
<span className="max-w-[8rem] truncate">#{filter.tag}</span>
|
||||||
<Icon.X className="w-4 h-auto ml-1" />
|
<Icon.X className="w-4 h-auto ml-1" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
62
web/src/components/Navigator.tsx
Normal file
62
web/src/components/Navigator.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
|
import { useAppSelector } from "../stores";
|
||||||
|
import useViewStore from "../stores/v1/view";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
const Navigator = () => {
|
||||||
|
const viewStore = useViewStore();
|
||||||
|
const { shortcutList } = useAppSelector((state) => state.shortcut);
|
||||||
|
const tags = shortcutList.map((shortcut) => shortcut.tags).flat();
|
||||||
|
const currentTab = viewStore.filter.tab || `tab:all`;
|
||||||
|
const sortedTagMap = sortTags(tags);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-row justify-start items-center mb-4 gap-1 sm:flex-wrap overflow-x-auto no-scrollbar">
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
"flex flex-row justify-center items-center px-2 leading-7 text-sm rounded-md hover:bg-gray-200",
|
||||||
|
currentTab === "tab:all" ? "!bg-gray-600 text-white shadow" : ""
|
||||||
|
)}
|
||||||
|
onClick={() => viewStore.setFilter({ tab: "tab:all" })}
|
||||||
|
>
|
||||||
|
<Icon.CircleSlash className="w-4 h-auto mr-1" />
|
||||||
|
<span className="font-normal">All</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
"flex flex-row justify-center items-center px-2 leading-7 text-sm rounded-md hover:bg-gray-200",
|
||||||
|
currentTab === "tab:mine" ? "!bg-gray-600 text-white shadow" : ""
|
||||||
|
)}
|
||||||
|
onClick={() => viewStore.setFilter({ tab: "tab:mine" })}
|
||||||
|
>
|
||||||
|
<Icon.User className="w-4 h-auto mr-1" />
|
||||||
|
<span className="font-normal">Mine</span>
|
||||||
|
</button>
|
||||||
|
{Array.from(sortedTagMap.keys()).map((tag) => (
|
||||||
|
<button
|
||||||
|
key={tag}
|
||||||
|
className={classNames(
|
||||||
|
"flex flex-row justify-center items-center px-2 leading-7 text-sm rounded-md hover:bg-gray-200",
|
||||||
|
currentTab === `tag:${tag}` ? "!bg-gray-600 text-white shadow" : ""
|
||||||
|
)}
|
||||||
|
onClick={() => viewStore.setFilter({ tab: `tag:${tag}`, tag: undefined })}
|
||||||
|
>
|
||||||
|
<Icon.Hash className="w-4 h-auto mr-0.5" />
|
||||||
|
<span className="max-w-[8rem] truncate font-normal">{tag}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortTags = (tags: string[]): Map<string, number> => {
|
||||||
|
const map = new Map<string, number>();
|
||||||
|
for (const tag of tags) {
|
||||||
|
const count = map.get(tag) || 0;
|
||||||
|
map.set(tag, count + 1);
|
||||||
|
}
|
||||||
|
const sortedMap = new Map([...map.entries()].sort((a, b) => b[1] - a[1]));
|
||||||
|
return sortedMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navigator;
|
@ -31,6 +31,7 @@ const ShortcutView = (props: Props) => {
|
|||||||
const [showAnalyticsDialog, setShowAnalyticsDialog] = useState<boolean>(false);
|
const [showAnalyticsDialog, setShowAnalyticsDialog] = useState<boolean>(false);
|
||||||
const havePermission = currentUser.role === "ADMIN" || shortcut.creatorId === currentUser.id;
|
const havePermission = currentUser.role === "ADMIN" || shortcut.creatorId === currentUser.id;
|
||||||
const shortcutLink = absolutifyLink(`/s/${shortcut.name}`);
|
const shortcutLink = absolutifyLink(`/s/${shortcut.name}`);
|
||||||
|
const compactStyle = viewStore.layout === "grid";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
faviconStore.getOrFetchUrlFavicon(shortcut.link).then((url) => {
|
faviconStore.getOrFetchUrlFavicon(shortcut.link).then((url) => {
|
||||||
@ -58,7 +59,7 @@ const ShortcutView = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full flex flex-col justify-start items-start border px-4 py-3 mb-2 rounded-lg hover:shadow">
|
<div className="w-full flex flex-col justify-start items-start border px-4 py-3 rounded-lg hover:shadow">
|
||||||
<div className="w-full flex flex-row justify-between items-center">
|
<div className="w-full flex flex-row justify-between items-center">
|
||||||
<div className="group flex flex-row justify-start items-center pr-2 mr-1 shrink-0">
|
<div className="group flex flex-row justify-start items-center pr-2 mr-1 shrink-0">
|
||||||
<div className="w-6 h-6 mr-1 flex justify-center items-center overflow-clip">
|
<div className="w-6 h-6 mr-1 flex justify-center items-center overflow-clip">
|
||||||
@ -74,7 +75,7 @@ const ShortcutView = (props: Props) => {
|
|||||||
href={shortcutLink}
|
href={shortcutLink}
|
||||||
>
|
>
|
||||||
<span className="text-gray-400">s/</span>
|
<span className="text-gray-400">s/</span>
|
||||||
{shortcut.name}
|
<span className="max-w-[14rem] truncate">{shortcut.name}</span>
|
||||||
<span className="hidden group-hover:block ml-1 cursor-pointer">
|
<span className="hidden group-hover:block ml-1 cursor-pointer">
|
||||||
<Icon.ExternalLink className="w-4 h-auto text-gray-600" />
|
<Icon.ExternalLink className="w-4 h-auto text-gray-600" />
|
||||||
</span>
|
</span>
|
||||||
@ -128,23 +129,21 @@ const ShortcutView = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{shortcut.description && <p className="mt-1 text-gray-400 text-sm">{shortcut.description}</p>}
|
{shortcut.description && !compactStyle && <p className="w-full break-all mt-1 text-gray-400 text-sm">{shortcut.description}</p>}
|
||||||
{shortcut.tags.length > 0 && (
|
<div className="mt-2 flex flex-row justify-start items-start flex-wrap gap-2">
|
||||||
<div className="mt-2 ml-1 flex flex-row justify-start items-start gap-2">
|
|
||||||
<Icon.Tag className="text-gray-400 w-4 h-auto" />
|
|
||||||
{shortcut.tags.map((tag) => {
|
{shortcut.tags.map((tag) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={tag}
|
key={tag}
|
||||||
className="text-gray-400 text-sm font-mono leading-4 cursor-pointer hover:text-gray-600"
|
className="max-w-[8rem] truncate text-gray-400 text-sm font-mono leading-4 cursor-pointer hover:text-gray-600"
|
||||||
onClick={() => viewStore.setFilter({ tag: tag })}
|
onClick={() => viewStore.setFilter({ tag: tag })}
|
||||||
>
|
>
|
||||||
#{tag}
|
#{tag}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{shortcut.tags.length === 0 && <span className="text-gray-400 text-sm font-mono leading-4 italic">No tags</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="w-full flex mt-2 gap-2">
|
<div className="w-full flex mt-2 gap-2">
|
||||||
<Tooltip title="Creator" variant="solid" placement="top" arrow>
|
<Tooltip title="Creator" variant="solid" placement="top" arrow>
|
||||||
<div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border rounded-full text-gray-500 text-sm">
|
<div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border rounded-full text-gray-500 text-sm">
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import CreateShortcutDialog from "./CreateShortcutDialog";
|
import CreateShortcutDialog from "./CreateShortcutDialog";
|
||||||
import ShortcutView from "./ShortcutView";
|
import ShortcutView from "./ShortcutView";
|
||||||
|
import useViewStore from "../stores/v1/view";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
shortcutList: Shortcut[];
|
shortcutList: Shortcut[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortcutListView: React.FC<Props> = (props: Props) => {
|
const ShortcutsContainer: React.FC<Props> = (props: Props) => {
|
||||||
const { shortcutList } = props;
|
const { shortcutList } = props;
|
||||||
|
const viewStore = useViewStore();
|
||||||
|
const layout = viewStore.layout || "list";
|
||||||
const [editingShortcutId, setEditingShortcutId] = useState<ShortcutId | undefined>();
|
const [editingShortcutId, setEditingShortcutId] = useState<ShortcutId | undefined>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full flex flex-col justify-start items-start">
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"w-full flex flex-col justify-start items-start gap-y-2",
|
||||||
|
layout === "grid" && "sm:grid sm:grid-cols-2 sm:gap-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{shortcutList.map((shortcut) => {
|
{shortcutList.map((shortcut) => {
|
||||||
return <ShortcutView key={shortcut.id} shortcut={shortcut} handleEdit={() => setEditingShortcutId(shortcut.id)} />;
|
return <ShortcutView key={shortcut.id} shortcut={shortcut} handleEdit={() => setEditingShortcutId(shortcut.id)} />;
|
||||||
})}
|
})}
|
||||||
@ -31,4 +40,4 @@ const ShortcutListView: React.FC<Props> = (props: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShortcutListView;
|
export default ShortcutsContainer;
|
@ -4,10 +4,11 @@ import useViewStore from "../stores/v1/view";
|
|||||||
import Dropdown from "./common/Dropdown";
|
import Dropdown from "./common/Dropdown";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
const OrderSetting = () => {
|
const ViewSetting = () => {
|
||||||
const viewStore = useViewStore();
|
const viewStore = useViewStore();
|
||||||
const order = viewStore.getOrder();
|
const order = viewStore.getOrder();
|
||||||
const { field, direction } = order;
|
const { field, direction } = order;
|
||||||
|
const layout = viewStore.layout || "list";
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
viewStore.setOrder({ field: "name", direction: "asc" });
|
viewStore.setOrder({ field: "name", direction: "asc" });
|
||||||
@ -17,10 +18,11 @@ const OrderSetting = () => {
|
|||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
trigger={
|
trigger={
|
||||||
<button className="p-1 mr-2">
|
<button className="p-1">
|
||||||
<Icon.ListFilter className="w-5 h-auto text-gray-500" />
|
<Icon.ListFilter className="w-5 h-auto text-gray-500" />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
actionsClassName="right-10 translate-x-full"
|
||||||
actions={
|
actions={
|
||||||
<div className="w-52 p-2 pt-0 gap-2 flex flex-col justify-start items-start" onClick={(e) => e.stopPropagation()}>
|
<div className="w-52 p-2 pt-0 gap-2 flex flex-col justify-start items-start" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="w-full flex flex-row justify-between items-center mt-1">
|
<div className="w-full flex flex-row justify-between items-center mt-1">
|
||||||
@ -45,10 +47,17 @@ const OrderSetting = () => {
|
|||||||
<Option value={"desc"}>DESC</Option>
|
<Option value={"desc"}>DESC</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-full flex flex-row justify-between items-center">
|
||||||
|
<span className="text-sm shrink-0 mr-2">Layout</span>
|
||||||
|
<Select size="sm" value={layout} onChange={(_, value) => viewStore.setLayout(value as any)}>
|
||||||
|
<Option value={"list"}>List</Option>
|
||||||
|
<Option value={"grid"}>Grid</Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
></Dropdown>
|
></Dropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OrderSetting;
|
export default ViewSetting;
|
@ -10,3 +10,18 @@ html,
|
|||||||
"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";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
@variants responsive {
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { useEffect } from "react";
|
|||||||
import { Outlet, useNavigate } from "react-router-dom";
|
import { Outlet, useNavigate } from "react-router-dom";
|
||||||
import useUserStore from "../stores/v1/user";
|
import useUserStore from "../stores/v1/user";
|
||||||
import Header from "../components/Header";
|
import Header from "../components/Header";
|
||||||
import DemoBanner from "../components/DemoBanner";
|
|
||||||
|
|
||||||
const Root: React.FC = () => {
|
const Root: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -20,7 +19,6 @@ const Root: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
<div className="w-full h-full flex flex-col justify-start items-start">
|
<div className="w-full h-full flex flex-col justify-start items-start">
|
||||||
<DemoBanner />
|
|
||||||
<Header />
|
<Header />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button, Input, Tab, TabList, Tabs } from "@mui/joy";
|
import { Button, Input } from "@mui/joy";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { shortcutService } from "../services";
|
import { shortcutService } from "../services";
|
||||||
import { useAppSelector } from "../stores";
|
import { useAppSelector } from "../stores";
|
||||||
@ -6,10 +6,11 @@ import useViewStore, { getFilteredShortcutList, getOrderedShortcutList } from ".
|
|||||||
import useUserStore from "../stores/v1/user";
|
import useUserStore from "../stores/v1/user";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
import ShortcutListView from "../components/ShortcutListView";
|
import ShortcutsContainer from "../components/ShortcutsContainer";
|
||||||
import CreateShortcutDialog from "../components/CreateShortcutDialog";
|
import CreateShortcutDialog from "../components/CreateShortcutDialog";
|
||||||
import FilterView from "../components/FilterView";
|
import FilterView from "../components/FilterView";
|
||||||
import OrderSetting from "../components/OrderSetting";
|
import ViewSetting from "../components/ViewSetting";
|
||||||
|
import Navigator from "../components/Navigator";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
showCreateShortcutDialog: boolean;
|
showCreateShortcutDialog: boolean;
|
||||||
@ -42,11 +43,12 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start">
|
<div className="mx-auto max-w-4xl w-full px-3 pt-4 pb-6 flex flex-col justify-start items-start">
|
||||||
|
<Navigator />
|
||||||
<div className="w-full flex flex-row justify-between items-center mb-4">
|
<div className="w-full flex flex-row justify-between items-center mb-4">
|
||||||
<span className="font-mono text-gray-400 mr-2">Shortcuts</span>
|
<div className="flex flex-row justify-start items-center">
|
||||||
<Input
|
<Input
|
||||||
className="w-32"
|
className="w-32 mr-2"
|
||||||
type="text"
|
type="text"
|
||||||
size="sm"
|
size="sm"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
@ -57,30 +59,16 @@ const Home: React.FC = () => {
|
|||||||
value={filter.search}
|
value={filter.search}
|
||||||
onChange={(e) => viewStore.setFilter({ search: e.target.value })}
|
onChange={(e) => viewStore.setFilter({ search: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
<ViewSetting />
|
||||||
<div className="w-full flex flex-row justify-between items-center mb-4">
|
|
||||||
<div className="flex flex-row justify-start items-center">
|
|
||||||
<Button className="hover:shadow" variant="soft" size="sm" onClick={() => setShowCreateShortcutDialog(true)}>
|
|
||||||
<Icon.Plus className="w-5 h-auto" /> New
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-end items-center">
|
<div className="flex flex-row justify-end items-center">
|
||||||
<OrderSetting />
|
<Button className="hover:shadow" variant="soft" size="sm" onClick={() => setShowCreateShortcutDialog(true)}>
|
||||||
<Tabs
|
<Icon.Plus className="w-5 h-auto" />
|
||||||
value={filter.mineOnly ? "PRIVATE" : "ALL"}
|
<span className="hidden sm:block ml-0.5">Create</span>
|
||||||
size="sm"
|
</Button>
|
||||||
onChange={(_, value) => viewStore.setFilter({ mineOnly: value !== "ALL" })}
|
|
||||||
>
|
|
||||||
<TabList>
|
|
||||||
<Tab value={"ALL"}>All</Tab>
|
|
||||||
<Tab value={"PRIVATE"}>Mine</Tab>
|
|
||||||
</TabList>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FilterView />
|
<FilterView />
|
||||||
|
|
||||||
{loadingState.isLoading ? (
|
{loadingState.isLoading ? (
|
||||||
<div className="py-12 w-full flex flex-row justify-center items-center opacity-80">
|
<div className="py-12 w-full flex flex-row justify-center items-center opacity-80">
|
||||||
<Icon.Loader className="mr-2 w-5 h-auto animate-spin" />
|
<Icon.Loader className="mr-2 w-5 h-auto animate-spin" />
|
||||||
@ -92,7 +80,7 @@ const Home: React.FC = () => {
|
|||||||
<p className="mt-4">No shortcuts found.</p>
|
<p className="mt-4">No shortcuts found.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ShortcutListView shortcutList={orderedShortcutList} />
|
<ShortcutsContainer shortcutList={orderedShortcutList} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@ import { create } from "zustand";
|
|||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
|
tab?: string;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
mineOnly?: boolean;
|
|
||||||
visibility?: Visibility;
|
visibility?: Visibility;
|
||||||
search?: string;
|
search?: string;
|
||||||
}
|
}
|
||||||
@ -13,12 +13,16 @@ export interface Order {
|
|||||||
direction: "asc" | "desc";
|
direction: "asc" | "desc";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Layout = "grid" | "list";
|
||||||
|
|
||||||
interface ViewState {
|
interface ViewState {
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
order: Order;
|
order: Order;
|
||||||
|
layout: Layout;
|
||||||
setFilter: (filter: Partial<Filter>) => void;
|
setFilter: (filter: Partial<Filter>) => void;
|
||||||
getOrder: () => Order;
|
getOrder: () => Order;
|
||||||
setOrder: (order: Partial<Order>) => void;
|
setOrder: (order: Partial<Order>) => void;
|
||||||
|
setLayout: (layout: Layout) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useViewStore = create<ViewState>()(
|
const useViewStore = create<ViewState>()(
|
||||||
@ -29,6 +33,7 @@ const useViewStore = create<ViewState>()(
|
|||||||
field: "name",
|
field: "name",
|
||||||
direction: "asc",
|
direction: "asc",
|
||||||
},
|
},
|
||||||
|
layout: "list",
|
||||||
setFilter: (filter: Partial<Filter>) => {
|
setFilter: (filter: Partial<Filter>) => {
|
||||||
set({ filter: { ...get().filter, ...filter } });
|
set({ filter: { ...get().filter, ...filter } });
|
||||||
},
|
},
|
||||||
@ -41,6 +46,9 @@ const useViewStore = create<ViewState>()(
|
|||||||
setOrder: (order: Partial<Order>) => {
|
setOrder: (order: Partial<Order>) => {
|
||||||
set({ order: { ...get().order, ...order } });
|
set({ order: { ...get().order, ...order } });
|
||||||
},
|
},
|
||||||
|
setLayout: (layout: Layout) => {
|
||||||
|
set({ layout });
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "view",
|
name: "view",
|
||||||
@ -49,18 +57,13 @@ const useViewStore = create<ViewState>()(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const getFilteredShortcutList = (shortcutList: Shortcut[], filter: Filter, currentUser: User) => {
|
export const getFilteredShortcutList = (shortcutList: Shortcut[], filter: Filter, currentUser: User) => {
|
||||||
const { tag, mineOnly, visibility, search } = filter;
|
const { tab, tag, visibility, search } = filter;
|
||||||
const filteredShortcutList = shortcutList.filter((shortcut) => {
|
const filteredShortcutList = shortcutList.filter((shortcut) => {
|
||||||
if (tag) {
|
if (tag) {
|
||||||
if (!shortcut.tags.includes(tag)) {
|
if (!shortcut.tags.includes(tag)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mineOnly) {
|
|
||||||
if (shortcut.creatorId !== currentUser.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (visibility) {
|
if (visibility) {
|
||||||
if (shortcut.visibility !== visibility) {
|
if (shortcut.visibility !== visibility) {
|
||||||
return false;
|
return false;
|
||||||
@ -76,6 +79,14 @@ export const getFilteredShortcutList = (shortcutList: Shortcut[], filter: Filter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (tab) {
|
||||||
|
if (tab === "tab:mine") {
|
||||||
|
return shortcut.creatorId === currentUser.id;
|
||||||
|
} else if (tab.startsWith("tag:")) {
|
||||||
|
const tag = tab.split(":")[1];
|
||||||
|
return shortcut.tags.includes(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
return filteredShortcutList;
|
return filteredShortcutList;
|
||||||
|
Reference in New Issue
Block a user