mirror of
https://github.com/aykhans/slash-e.git
synced 2025-07-06 13:12:36 +00:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
d42d3fbe10 | |||
6dfccb9509 | |||
66876452e1 | |||
6b107924aa | |||
b84620c057 | |||
c30b6adb8e | |||
c8fea442d6 | |||
a36a99e53d | |||
86078b097d | |||
11205566ac | |||
709118464b | |||
792b60c480 | |||
1418fc2209 |
12
README.md
12
README.md
@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
<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 short link service that allows you to save and share links easily. It lets you store and categorize links, generate short URLs for easy sharing, search and filter your saved links, and access them from any device.
|
`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.
|
||||||
|
|
||||||
Try it out on <a href="https://slash.stevenlgtm.com">Live Demo</a>.
|
Try it out on <a href="https://slash.stevenlgtm.com">Live Demo</a>.
|
||||||
|
|
||||||
|
<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://hub.docker.com/r/stevenlgtm/slash"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/stevenlgtm/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>
|
||||||
|
</p>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Create customizable `/s/` short links for any URL.
|
- Create customizable `/s/` short links for any URL.
|
||||||
@ -15,8 +21,8 @@ Try it out on <a href="https://slash.stevenlgtm.com">Live Demo</a>.
|
|||||||
|
|
||||||
## Deploy with Docker in seconds
|
## Deploy with Docker in seconds
|
||||||
|
|
||||||
> This project is under active development.
|
|
||||||
|
|
||||||
```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 stevenlgtm/slash:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Learn more in [Self-hosting Slash with Docker](https://github.com/boojack/slash/blob/main/docs/install.md).
|
||||||
|
@ -3,6 +3,7 @@ package v1
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
@ -49,7 +50,7 @@ func (s *APIV1Service) registerRedirectorRoutes(g *echo.Group) {
|
|||||||
|
|
||||||
func redirectToShortcut(c echo.Context, shortcut *store.Shortcut) error {
|
func redirectToShortcut(c echo.Context, shortcut *store.Shortcut) error {
|
||||||
isValidURL := isValidURLString(shortcut.Link)
|
isValidURL := isValidURLString(shortcut.Link)
|
||||||
if shortcut.OpenGraphMetadata == nil {
|
if shortcut.OpenGraphMetadata == nil || (shortcut.OpenGraphMetadata.Title == "" && shortcut.OpenGraphMetadata.Description == "" && shortcut.OpenGraphMetadata.Image == "") {
|
||||||
if isValidURL {
|
if isValidURL {
|
||||||
return c.Redirect(http.StatusSeeOther, shortcut.Link)
|
return c.Redirect(http.StatusSeeOther, shortcut.Link)
|
||||||
}
|
}
|
||||||
@ -63,6 +64,7 @@ func redirectToShortcut(c echo.Context, shortcut *store.Shortcut) error {
|
|||||||
fmt.Sprintf(`<meta property="og:title" content="%s" />`, shortcut.OpenGraphMetadata.Title),
|
fmt.Sprintf(`<meta property="og:title" content="%s" />`, shortcut.OpenGraphMetadata.Title),
|
||||||
fmt.Sprintf(`<meta property="og:description" content="%s" />`, shortcut.OpenGraphMetadata.Description),
|
fmt.Sprintf(`<meta property="og:description" content="%s" />`, shortcut.OpenGraphMetadata.Description),
|
||||||
fmt.Sprintf(`<meta property="og:image" content="%s" />`, shortcut.OpenGraphMetadata.Image),
|
fmt.Sprintf(`<meta property="og:image" content="%s" />`, shortcut.OpenGraphMetadata.Image),
|
||||||
|
`<meta property="og:type" content="website" />`,
|
||||||
// Twitter related metadata.
|
// Twitter related metadata.
|
||||||
fmt.Sprintf(`<meta name="twitter:title" content="%s" />`, shortcut.OpenGraphMetadata.Title),
|
fmt.Sprintf(`<meta name="twitter:title" content="%s" />`, shortcut.OpenGraphMetadata.Title),
|
||||||
fmt.Sprintf(`<meta name="twitter:description" content="%s" />`, shortcut.OpenGraphMetadata.Description),
|
fmt.Sprintf(`<meta name="twitter:description" content="%s" />`, shortcut.OpenGraphMetadata.Description),
|
||||||
@ -76,7 +78,7 @@ func redirectToShortcut(c echo.Context, shortcut *store.Shortcut) error {
|
|||||||
if isValidURL {
|
if isValidURL {
|
||||||
body = fmt.Sprintf(`<script>window.location.href = "%s";</script>`, shortcut.Link)
|
body = fmt.Sprintf(`<script>window.location.href = "%s";</script>`, shortcut.Link)
|
||||||
} else {
|
} else {
|
||||||
body = shortcut.Link
|
body = html.EscapeString(shortcut.Link)
|
||||||
}
|
}
|
||||||
htmlString := fmt.Sprintf(htmlTemplate, strings.Join(metadataList, ""), body)
|
htmlString := fmt.Sprintf(htmlTemplate, strings.Join(metadataList, ""), body)
|
||||||
return c.HTML(http.StatusOK, htmlString)
|
return c.HTML(http.StatusOK, htmlString)
|
||||||
|
39
docs/install.md
Normal file
39
docs/install.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Self-hosting Slash with Docker
|
||||||
|
|
||||||
|
Slash is designed for self-hosting through Docker. No Docker expertise is required to launch your own instance. Just basic understanding of command line and networking.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
The only requirement is a server with Docker installed.
|
||||||
|
|
||||||
|
## Docker Run
|
||||||
|
|
||||||
|
To deploy Slash using docker run, just one command is needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name slash --publish 5231:5231 --volume ~/.slash/:/var/opt/slash stevenlgtm/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.
|
||||||
|
|
||||||
|
## Upgrade
|
||||||
|
|
||||||
|
To upgrade Slash to latest version, stop and remove the old container first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stop slash && docker rm slash
|
||||||
|
```
|
||||||
|
|
||||||
|
It's recommended but optional to backup database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r ~/.slash/slash_prod.db ~/.slash/slash_prod.db.bak
|
||||||
|
```
|
||||||
|
|
||||||
|
Then pull the latest image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull stevenlgtm/slash:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, restart Slash by following the steps in [Docker Run](#docker-run).
|
@ -9,10 +9,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.3.0"
|
var Version = "0.3.1"
|
||||||
|
|
||||||
// DevVersion is the service current development version.
|
// DevVersion is the service current development version.
|
||||||
var DevVersion = "0.3.0"
|
var DevVersion = "0.3.1"
|
||||||
|
|
||||||
func GetCurrentVersion(mode string) string {
|
func GetCurrentVersion(mode string) string {
|
||||||
if mode == "dev" || mode == "demo" {
|
if mode == "dev" || mode == "demo" {
|
||||||
|
@ -12,8 +12,7 @@ VALUES
|
|||||||
'ADMIN',
|
'ADMIN',
|
||||||
'slash@stevenlgtm.com',
|
'slash@stevenlgtm.com',
|
||||||
'Slasher',
|
'Slasher',
|
||||||
-- raw password: secret
|
'$2a$10$H8HBWGcG/hoePhFy5SiNKOHxMD6omIpyEEWbl/fIorFC814bXW.Ua'
|
||||||
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
|
@ -10,28 +10,11 @@ VALUES
|
|||||||
(
|
(
|
||||||
1,
|
1,
|
||||||
101,
|
101,
|
||||||
'memos',
|
'discord',
|
||||||
'https://usememos.com',
|
'https://discord.gg/QZqUuUAhDV',
|
||||||
'PUBLIC'
|
'PUBLIC'
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO
|
|
||||||
shortcut (
|
|
||||||
`id`,
|
|
||||||
`creator_id`,
|
|
||||||
`name`,
|
|
||||||
`link`,
|
|
||||||
`visibility`
|
|
||||||
)
|
|
||||||
VALUES
|
|
||||||
(
|
|
||||||
2,
|
|
||||||
101,
|
|
||||||
'sqlchat',
|
|
||||||
'https://www.sqlchat.ai',
|
|
||||||
'WORKSPACE'
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
shortcut (
|
shortcut (
|
||||||
`id`,
|
`id`,
|
||||||
@ -43,7 +26,7 @@ INSERT INTO
|
|||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
3,
|
2,
|
||||||
101,
|
101,
|
||||||
'ai-infra',
|
'ai-infra',
|
||||||
'https://star-history.com/blog/open-source-ai-infra-projects',
|
'https://star-history.com/blog/open-source-ai-infra-projects',
|
||||||
@ -62,7 +45,7 @@ INSERT INTO
|
|||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
4,
|
3,
|
||||||
101,
|
101,
|
||||||
'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',
|
||||||
@ -70,6 +53,23 @@ VALUES
|
|||||||
'{"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"}'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
INSERT INTO
|
||||||
|
shortcut (
|
||||||
|
`id`,
|
||||||
|
`creator_id`,
|
||||||
|
`name`,
|
||||||
|
`link`,
|
||||||
|
`visibility`
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
4,
|
||||||
|
101,
|
||||||
|
'sqlchat',
|
||||||
|
'https://www.sqlchat.ai',
|
||||||
|
'WORKSPACE'
|
||||||
|
);
|
||||||
|
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
shortcut (
|
shortcut (
|
||||||
`id`,
|
`id`,
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"@mui/joy": "5.0.0-alpha.84",
|
"@mui/joy": "5.0.0-alpha.84",
|
||||||
"@reduxjs/toolkit": "^1.8.1",
|
"@reduxjs/toolkit": "^1.8.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
"copy-to-clipboard": "^3.3.2",
|
"copy-to-clipboard": "^3.3.2",
|
||||||
"dayjs": "^1.11.3",
|
"dayjs": "^1.11.3",
|
||||||
"i18next": "^23.2.3",
|
"i18next": "^23.2.3",
|
||||||
|
7
web/pnpm-lock.yaml
generated
7
web/pnpm-lock.yaml
generated
@ -20,6 +20,9 @@ dependencies:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^0.27.2
|
specifier: ^0.27.2
|
||||||
version: 0.27.2
|
version: 0.27.2
|
||||||
|
classnames:
|
||||||
|
specifier: ^2.3.2
|
||||||
|
version: 2.3.2
|
||||||
copy-to-clipboard:
|
copy-to-clipboard:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
@ -1263,6 +1266,10 @@ packages:
|
|||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/classnames@2.3.2:
|
||||||
|
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/clsx@1.2.1:
|
/clsx@1.2.1:
|
||||||
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
|
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -19,12 +19,11 @@ 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> is a bookmarking and short link service that allows you to save and share links
|
<span className="font-medium">Slash</span>: A bookmarking and url shortener, save and share your links very easily.
|
||||||
easily.
|
|
||||||
</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>
|
||||||
<Link variant="plain" href="https://github.com/boojack/slash">
|
<Link variant="plain" href="https://github.com/boojack/slash" target="_blank">
|
||||||
GitHub
|
GitHub
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Button, Divider, Input, Modal, ModalDialog, Radio, RadioGroup, Textarea } from "@mui/joy";
|
import { Button, Divider, Input, Modal, ModalDialog, Radio, RadioGroup, Textarea } from "@mui/joy";
|
||||||
|
import classnames from "classnames";
|
||||||
import { isUndefined } from "lodash-es";
|
import { isUndefined } from "lodash-es";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -182,7 +183,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<Icon.X className="w-5 h-auto text-gray-600" />
|
<Icon.X className="w-5 h-auto text-gray-600" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto">
|
<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 className="text-red-600">*</span>
|
Name <span className="text-red-600">*</span>
|
||||||
@ -227,14 +228,15 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<Divider className="text-gray-500">Optional</Divider>
|
<Divider className="text-gray-500">Optional</Divider>
|
||||||
<div className="w-full flex flex-col justify-start items-start border rounded-md overflow-hidden my-3">
|
<div className="w-full flex flex-col justify-start items-start border rounded-md overflow-hidden my-3">
|
||||||
<div
|
<div
|
||||||
className={`w-full flex flex-row justify-between items-center px-2 py-1 cursor-pointer hover:bg-gray-100 ${
|
className={classnames(
|
||||||
showDescriptionAndTag ? "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" : ""
|
||||||
|
)}
|
||||||
onClick={() => setShowDescriptionAndTag(!showDescriptionAndTag)}
|
onClick={() => setShowDescriptionAndTag(!showDescriptionAndTag)}
|
||||||
>
|
>
|
||||||
<span className="text-sm">Description and tags</span>
|
<span className="text-sm">Description and tags</span>
|
||||||
<button className="w-7 h-7 p-1 rounded-md">
|
<button className="w-7 h-7 p-1 rounded-md">
|
||||||
<Icon.ChevronDown className={`w-4 h-auto text-gray-500 ${showDescriptionAndTag ? "transform rotate-180" : ""}`} />
|
<Icon.ChevronDown className={classnames("w-4 h-auto text-gray-500", showDescriptionAndTag ? "transform rotate-180" : "")} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{showDescriptionAndTag && (
|
{showDescriptionAndTag && (
|
||||||
@ -267,7 +269,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<div className="w-full flex flex-col justify-start items-start border rounded-md overflow-hidden">
|
<div className="w-full flex flex-col justify-start items-start border rounded-md overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`w-full flex flex-row justify-between items-center px-2 py-1 cursor-pointer hover:bg-gray-100 ${
|
className={`w-full flex flex-row justify-between items-center px-2 py-1 cursor-pointer hover:bg-gray-100 ${
|
||||||
showOpenGraphMetadata ? "bg-gray-100" : ""
|
showOpenGraphMetadata ? "bg-gray-100 border-b" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setShowOpenGraphMetadata(!showOpenGraphMetadata)}
|
onClick={() => setShowOpenGraphMetadata(!showOpenGraphMetadata)}
|
||||||
>
|
>
|
||||||
@ -276,7 +278,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<Icon.Sparkles className="ml-1 w-4 h-auto text-blue-600" />
|
<Icon.Sparkles className="ml-1 w-4 h-auto text-blue-600" />
|
||||||
</span>
|
</span>
|
||||||
<button className="w-7 h-7 p-1 rounded-md">
|
<button className="w-7 h-7 p-1 rounded-md">
|
||||||
<Icon.ChevronDown className={`w-4 h-auto text-gray-500 ${showDescriptionAndTag ? "transform rotate-180" : ""}`} />
|
<Icon.ChevronDown className={classnames("w-4 h-auto text-gray-500", showOpenGraphMetadata ? "transform rotate-180" : "")} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{showOpenGraphMetadata && (
|
{showOpenGraphMetadata && (
|
||||||
|
@ -149,7 +149,7 @@ const ShortcutView = (props: Props) => {
|
|||||||
<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">
|
||||||
<Icon.User className="w-4 h-auto mr-1" />
|
<Icon.User className="w-4 h-auto mr-1" />
|
||||||
{shortcut.creator.nickname}
|
<span className="max-w-[4rem] sm:max-w-[6rem] truncate">{shortcut.creator.nickname}</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.description`)} variant="solid" placement="top" arrow>
|
<Tooltip title={t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.description`)} variant="solid" placement="top" arrow>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button, Tab, TabList, Tabs } from "@mui/joy";
|
import { Button, Input, Tab, TabList, Tabs } 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";
|
||||||
@ -43,8 +43,20 @@ 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 py-6 flex flex-col justify-start items-start">
|
||||||
<div className="w-full flex flex-row justify-start 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>
|
<span className="font-mono text-gray-400 mr-2">Shortcuts</span>
|
||||||
|
<Input
|
||||||
|
className="w-32"
|
||||||
|
type="text"
|
||||||
|
size="sm"
|
||||||
|
placeholder="Search"
|
||||||
|
startDecorator={<Icon.Search className="w-4 h-auto" />}
|
||||||
|
endDecorator={
|
||||||
|
filter.search && <Icon.X className="w-4 h-auto cursor-pointer" onClick={() => viewStore.setFilter({ search: "" })} />
|
||||||
|
}
|
||||||
|
value={filter.search}
|
||||||
|
onChange={(e) => viewStore.setFilter({ search: e.target.value })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
||||||
<div className="flex flex-row justify-start items-center">
|
<div className="flex flex-row justify-start items-center">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Button, Input } from "@mui/joy";
|
import { Button, Input } from "@mui/joy";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { FormEvent, useEffect, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import * as api from "../helpers/api";
|
import * as api from "../helpers/api";
|
||||||
@ -29,7 +29,7 @@ const SignIn: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mode === "demo") {
|
if (mode === "demo") {
|
||||||
setEmail("slash@stevenlgtm.com");
|
setEmail("steven@usememos.com");
|
||||||
setPassword("secret");
|
setPassword("secret");
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@ -44,7 +44,8 @@ const SignIn: React.FC = () => {
|
|||||||
setPassword(text);
|
setPassword(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSigninBtnClick = async () => {
|
const handleSigninBtnClick = async (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
if (actionBtnLoadingState.isLoading) {
|
if (actionBtnLoadingState.isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Button, Input } from "@mui/joy";
|
import { Button, Input } from "@mui/joy";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { FormEvent, useEffect, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import * as api from "../helpers/api";
|
import * as api from "../helpers/api";
|
||||||
@ -48,7 +48,8 @@ const SignUp: React.FC = () => {
|
|||||||
setPassword(text);
|
setPassword(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSignupBtnClick = async () => {
|
const handleSignupBtnClick = async (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
if (actionBtnLoadingState.isLoading) {
|
if (actionBtnLoadingState.isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ export interface Filter {
|
|||||||
tag?: string;
|
tag?: string;
|
||||||
mineOnly?: boolean;
|
mineOnly?: boolean;
|
||||||
visibility?: Visibility;
|
visibility?: Visibility;
|
||||||
|
search?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Order {
|
export interface Order {
|
||||||
@ -48,7 +49,7 @@ 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 } = filter;
|
const { tag, mineOnly, 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)) {
|
||||||
@ -65,6 +66,16 @@ export const getFilteredShortcutList = (shortcutList: Shortcut[], filter: Filter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (search) {
|
||||||
|
if (
|
||||||
|
!shortcut.name.toLowerCase().includes(search.toLowerCase()) &&
|
||||||
|
!shortcut.description.toLowerCase().includes(search.toLowerCase()) &&
|
||||||
|
!shortcut.tags.some((tag) => tag.toLowerCase().includes(search.toLowerCase())) &&
|
||||||
|
!shortcut.link.toLowerCase().includes(search.toLowerCase())
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
return filteredShortcutList;
|
return filteredShortcutList;
|
||||||
|
Reference in New Issue
Block a user