Merge branch 'yourselfhosted:main' into releases/v1.0.0-rc.0-e

This commit is contained in:
2026-04-25 17:34:05 +04:00
committed by GitHub
211 changed files with 27206 additions and 8951 deletions
+4 -4
View File
@@ -12,7 +12,7 @@ jobs:
go-static-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-go@v5
with:
go-version: 1.23
@@ -23,16 +23,16 @@ jobs:
go mod tidy
git diff --exit-code
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v7
with:
version: v1.61.0
version: v2.0.2
args: --verbose --timeout=3m
skip-cache: true
go-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-go@v5
with:
go-version: 1.23
@@ -16,7 +16,7 @@ jobs:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -8,7 +8,7 @@ jobs:
build-and-push-test-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v5
+10 -10
View File
@@ -14,13 +14,13 @@ jobs:
eslint-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4.0.0
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4.1.0
with:
version: 9
- uses: actions/setup-node@v4
version: 10
- uses: actions/setup-node@v6
with:
node-version: "18"
node-version: "22"
cache: pnpm
cache-dependency-path: "frontend/extension/pnpm-lock.yaml"
- run: pnpm install
@@ -32,13 +32,13 @@ jobs:
extension-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4.0.0
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4.1.0
with:
version: 9
- uses: actions/setup-node@v4
version: 10
- uses: actions/setup-node@v6
with:
node-version: "18"
node-version: "22"
cache: pnpm
cache-dependency-path: "frontend/extension/pnpm-lock.yaml"
- run: pnpm install
+10 -10
View File
@@ -14,13 +14,13 @@ jobs:
eslint-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4.0.0
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4.1.0
with:
version: 9
- uses: actions/setup-node@v4
version: 10
- uses: actions/setup-node@v6
with:
node-version: "18"
node-version: "22"
cache: pnpm
cache-dependency-path: "frontend/web/pnpm-lock.yaml"
- run: pnpm install
@@ -35,13 +35,13 @@ jobs:
frontend-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4.0.0
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4.1.0
with:
version: 9
- uses: actions/setup-node@v4
version: 10
- uses: actions/setup-node@v6
with:
node-version: "18"
node-version: "22"
cache: pnpm
cache-dependency-path: "frontend/web/pnpm-lock.yaml"
- run: pnpm install
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup buf
-3
View File
@@ -1,6 +1,3 @@
# Air (hot reload) generated
.air
# temp folder
tmp
+86 -75
View File
@@ -1,7 +1,7 @@
version: "2"
linters:
enable:
- errcheck
- goimports
- revive
- govet
- staticcheck
@@ -14,77 +14,88 @@ linters:
- forbidigo
- mirror
- bodyclose
disable:
- errcheck
settings:
exhaustive:
explicit-exhaustive-switch: false
staticcheck:
checks:
- all
- -ST1000
- -ST1003
- -ST1021
- -QF1003
revive:
# Default to run all linters so that new rules in the future could automatically be added to the static check.
enable-all-rules: true
rules:
# The following rules are too strict and make coding harder. We do not enable them for now.
- name: file-header
disabled: true
- name: line-length-limit
disabled: true
- name: function-length
disabled: true
- name: max-public-structs
disabled: true
- name: function-result-limit
disabled: true
- name: banned-characters
disabled: true
- name: argument-limit
disabled: true
- name: cognitive-complexity
disabled: true
- name: cyclomatic
disabled: true
- name: confusing-results
disabled: true
- name: add-constant
disabled: true
- name: flag-parameter
disabled: true
- name: nested-structs
disabled: true
- name: import-shadowing
disabled: true
- name: early-return
disabled: true
- name: use-any
disabled: true
- name: exported
disabled: true
- name: unhandled-error
disabled: true
- name: if-return
disabled: true
- name: max-control-nesting
disabled: true
- name: redefines-builtin-id
disabled: true
- name: package-comments
disabled: true
gocritic:
disabled-checks:
- ifElseChain
govet:
settings:
printf: # The name of the analyzer, run `go tool vet help` to see the list of all analyzers
funcs: # Run `go tool vet help printf` to see the full configuration of `printf`.
- common.Errorf
enable-all: true
disable:
- fieldalignment
- shadow
forbidigo:
forbid:
- pattern: 'fmt\.Errorf(# Please use errors\.Wrap\|Wrapf\|Errorf instead)?'
- pattern: 'ioutil\.ReadDir(# Please use os\.ReadDir)?'
issues:
include:
# https://golangci-lint.run/usage/configuration/#command-line-options
exclude:
- Rollback
- fmt.Printf
linters-settings:
goimports:
# Put imports beginning with prefix after 3rd-party packages.
local-prefixes: github.com/yourselfhosted/slash
revive:
# Default to run all linters so that new rules in the future could automatically be added to the static check.
enable-all-rules: true
rules:
# The following rules are too strict and make coding harder. We do not enable them for now.
- name: file-header
disabled: true
- name: line-length-limit
disabled: true
- name: function-length
disabled: true
- name: max-public-structs
disabled: true
- name: function-result-limit
disabled: true
- name: banned-characters
disabled: true
- name: argument-limit
disabled: true
- name: cognitive-complexity
disabled: true
- name: cyclomatic
disabled: true
- name: confusing-results
disabled: true
- name: add-constant
disabled: true
- name: flag-parameter
disabled: true
- name: nested-structs
disabled: true
- name: import-shadowing
disabled: true
- name: early-return
disabled: true
- name: use-any
disabled: true
- name: var-naming
disabled: true
- name: unchecked-type-assertion
disabled: true
- name: max-control-nesting
disabled: true
- name: exported
arguments:
- "disableStutteringCheck"
gocritic:
disabled-checks:
- ifElseChain
govet:
settings:
printf: # The name of the analyzer, run `go tool vet help` to see the list of all analyzers
funcs: # Run `go tool vet help printf` to see the full configuration of `printf`.
- common.Errorf
enable-all: true
disable:
- fieldalignment
- shadow
forbidigo:
forbid:
- 'fmt\.Errorf(# Please use errors\.Wrap\|Wrapf\|Errorf instead)?'
- 'ioutil\.ReadDir(# Please use os\.ReadDir)?'
formatters:
enable:
- goimports
settings:
goimports:
local-prefixes:
- github.com/usememos/memos
+2 -2
View File
@@ -89,11 +89,11 @@ var (
)
func init() {
viper.SetDefault("mode", "demo")
viper.SetDefault("mode", "dev")
viper.SetDefault("driver", "sqlite")
viper.SetDefault("port", 8082)
rootCmd.PersistentFlags().String("mode", "demo", `mode of server, can be "prod" or "dev" or "demo"`)
rootCmd.PersistentFlags().String("mode", "dev", `mode of server, can be "prod" or "dev"`)
rootCmd.PersistentFlags().String("addr", "", "address of server")
rootCmd.PersistentFlags().Int("port", 8082, "port of server")
rootCmd.PersistentFlags().String("data", "", "data directory")
+1 -1
View File
@@ -1,6 +1,6 @@
# Single Sign-On(SSO)
> **Note**: This feature is only available in the **Enterprise** plan.
> **Note**: This feature is only available in the **Team** plan.
**Single Sign-On (SSO)** is an authentication method that enables users to securely authenticate with multiple applications and websites by using just one set of credentials.
-2
View File
@@ -36,5 +36,3 @@ keys.json
# typescript
.tsbuildinfo
src/types/proto
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 yourselfhosted
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+29 -24
View File
@@ -1,44 +1,44 @@
{
"name": "slash-extension",
"displayName": "Slash",
"version": "1.0.11",
"description": "An open source, self-hosted platform for sharing and managing your most frequently used links. Save and share your links very easily.",
"version": "1.0.12",
"description": "An open source, self-hosted platform for sharing and managing your most frequently used links.",
"scripts": {
"dev": "plasmo dev",
"build": "plasmo build",
"build": "export PARCEL_WORKER_BACKEND=process && plasmo build",
"package": "plasmo package",
"lint": "eslint --ext .js,.ts,.tsx, src",
"lint-fix": "eslint --ext .js,.ts,.tsx, src --fix"
},
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@mui/joy": "5.0.0-beta.48",
"@plasmohq/storage": "^1.12.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/joy": "5.0.0-beta.51",
"@plasmohq/storage": "^1.15.0",
"classnames": "^2.5.1",
"lucide-react": "^0.454.0",
"plasmo": "^0.89.3",
"lucide-react": "^0.536.0",
"plasmo": "^0.90.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1"
"react-hot-toast": "^2.5.2"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/chrome": "^0.0.280",
"@types/node": "^22.8.6",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/chrome": "^0.1.3",
"@types/node": "^22.17.1",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"autoprefixer": "^10.4.20",
"autoprefixer": "^10.4.21",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.2",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3"
"eslint-config-prettier": "^9.1.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.9.2"
},
"manifest": {
"permissions": [
@@ -49,5 +49,10 @@
"host_permissions": [
"*://*/*"
]
},
"pnpm": {
"overrides": {
"@swc/core": "1.5.7"
}
}
}
}
+2138 -2108
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -18,7 +18,7 @@ chrome.webRequest.onBeforeRequest.addListener(
}
})();
},
{ urls: ["*://s/*", "*://*/search*"] },
{ urls: ["*://s/*", "*://*/search*", "*://*/s*", "*://duckduckgo.com/*"] },
);
const getShortcutNameFromUrl = (urlString: string) => {
+1 -2
View File
@@ -68,7 +68,6 @@
},
"workspace": {
"self": "Workspace settings",
"custom-style": "Custom style",
"disallow-user-registration": {
"self": "Disallow user registration"
},
@@ -79,4 +78,4 @@
}
}
}
}
}
+1 -2
View File
@@ -68,7 +68,6 @@
},
"workspace": {
"self": "Paramètres de l'espace de travail",
"custom-style": "Style personnalisé",
"enable-user-signup": {
"self": "Activer l'inscription des utilisateurs",
"description": "Une fois activé, d'autres utilisateurs peuvent s'inscrire."
@@ -76,4 +75,4 @@
"default-visibility": "Visibilité par défaut"
}
}
}
}
+1 -2
View File
@@ -69,7 +69,6 @@
},
"workspace": {
"self": "Munkaterület beállítások",
"custom-style": "Egyéni stílus",
"enable-user-signup": {
"self": "Felhasználói regisztráció engedélyezése",
"description": "Ha engedélyezve van, más felhasználók is regisztrálhatnak."
@@ -77,4 +76,4 @@
"default-visibility": "Alapértelmezett láthatóság"
}
}
}
}
+14 -13
View File
@@ -12,14 +12,16 @@
"search": "検索",
"email": "Eメール",
"password": "パスワード",
"account": "アカウント"
"account": "アカウント",
"or": "または"
},
"auth": {
"sign-in": "サインイン",
"sign-up": "登録",
"sign-out": "サインアウト",
"create-your-account": "アカウントを作成してください",
"host-tip": "管理者として登録されています。"
"host-tip": "管理者として登録されています。",
"sign-in-with": "{{provider}}でサインイン"
},
"analytics": {
"self": "分析",
@@ -46,7 +48,7 @@
},
"filter": {
"all": "全て",
"mine": "自分",
"personal": "個人",
"compact-mode": "コンパクトモード",
"order-by": "順序",
"direction": "方向"
@@ -56,10 +58,7 @@
"nickname": "ニックネーム",
"email": "Eメール",
"role": "役割",
"profile": "プロフィール",
"action": {
"add-user": "ユーザーの追加"
}
"profile": "プロフィール"
},
"settings": {
"self": "設定",
@@ -69,12 +68,14 @@
},
"workspace": {
"self": "ワークスペースの設定",
"custom-style": "カスタムスタイル",
"enable-user-signup": {
"self": "ユーザーの登録を有効にする",
"description": "有効にすると他のユーザーが登録できるようになります。"
"disallow-user-registration": {
"self": "ユーザーの登録を有効にする"
},
"default-visibility": "デフォルトの表示"
"default-visibility": "デフォルトの表示",
"member": {
"self": "メンバー",
"add": "メンバーを追加"
}
}
}
}
}
+1 -2
View File
@@ -69,7 +69,6 @@
},
"workspace": {
"self": "Настройки команды",
"custom-style": "Пользовательский стиль",
"enable-user-signup": {
"self": "Разрешить регистрацию пользователей",
"description": "После включения, другие пользователи смогут зарегистрироваться."
@@ -77,4 +76,4 @@
"default-visibility": "Отображение по умолчанию"
}
}
}
}
+1 -2
View File
@@ -68,7 +68,6 @@
},
"workspace": {
"self": "Çalışma alanı ayarları",
"custom-style": "Özel stil",
"enable-user-signup": {
"self": "Kullanıcı kaydını etkinleştir",
"description": "Etkinleştirildiğinde, diğer kullanıcılar kaydolabilir."
@@ -76,4 +75,4 @@
"default-visibility": "Varsayılan görünürlük"
}
}
}
}
+81
View File
@@ -0,0 +1,81 @@
{
"common": {
"about": "Про",
"loading": "Завантаження",
"cancel": "Скасувати",
"save": "Зберегти",
"create": "Створити",
"download": "Завантажити",
"edit": "Редагувати",
"delete": "Видалити",
"language": "Мова",
"search": "Пошук",
"email": "Електронна пошта",
"password": "Пароль",
"account": "Обліковий запис",
"or": "Або"
},
"auth": {
"sign-in": "Увійдіть",
"sign-up": "Зареєструватися",
"sign-out": "Вийти",
"create-your-account": "Створіть свій акаунт",
"host-tip": "Ви реєструєтесь як адміністратор.",
"sign-in-with": "Увійдіть за допомогою {{provider}}"
},
"analytics": {
"self": "Аналітика",
"top-sources": "Найпопулярніші джерела",
"source": "Джерело",
"visitors": "Відвідувачі",
"devices": "Пристрої",
"browser": "Браузер",
"browsers": "Браузери",
"operating-system": "Операційна система"
},
"shortcut": {
"visits": "{{count}} відвідувань",
"visibility": {
"workspace": {
"self": "Робоча область",
"description": "Учасники робочої області мають доступ"
},
"public": {
"self": "Відкритий",
"description": "Відкрито в Інтернеті"
}
}
},
"filter": {
"all": "Все",
"personal": "Особисті",
"compact-mode": "Компактний режим",
"order-by": "Сортувати за",
"direction": "Напрямок"
},
"user": {
"self": "Користувач",
"nickname": "Псевдонім",
"email": "Електронна пошта",
"role": "Роль",
"profile": "Профіль"
},
"settings": {
"self": "Налаштування",
"preference": {
"self": "Вибір",
"color-theme": "Кольорова тема"
},
"workspace": {
"self": "Налаштування робочого простору",
"disallow-user-registration": {
"self": "Заборонити реєстрацію користувача"
},
"default-visibility": "Видимість за замовченям",
"member": {
"self": "Учасник",
"add": "Додати учасника"
}
}
}
}
+1 -2
View File
@@ -68,7 +68,6 @@
},
"workspace": {
"self": "系统设置",
"custom-style": "自定义样式",
"enable-user-signup": {
"self": "启用用户注册",
"description": "允许其他用户注册新账号"
@@ -76,4 +75,4 @@
"default-visibility": "默认可见性"
}
}
}
}
-1
View File
@@ -3,4 +3,3 @@ node_modules
dist
dist-ssr
*.local
src/types/proto
-1
View File
@@ -1 +0,0 @@
# Slash
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/css/index.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components/ui",
"utils": "@/lib/utils"
}
}
+57
View File
@@ -0,0 +1,57 @@
# UI Components Guide
## Overview
This project uses [shadcn/ui](https://ui.shadcn.com) - a collection of re-usable components built with Radix UI and Tailwind CSS.
## Component Location
All UI primitives are in `src/components/ui/`. These are copied directly into the project, so you own the code.
## Available Components
### Primitives
- Button
- Input
- Label
- Textarea
- Checkbox
- Badge
- Card
- Separator
### Complex Components
- Dialog (Modal)
- Sheet (Drawer)
- Dropdown Menu
- Tooltip
- Avatar
- Alert Dialog
- Sonner (Toast notifications)
### Custom Components
- AnimatedCard (with Framer Motion)
## Adding New Components
Use the shadcn CLI:
```bash
cd frontend/web
npx shadcn@latest add [component-name]
```
## Styling
Components use CSS variables defined in `src/css/index.css`. To customize:
1. Edit CSS variables for colors
2. Modify `tailwind.config.js` for theme changes
3. Use the `cn()` utility from `@/lib/utils` to merge classes
## Design Principles
- **Clean & Minimal**: Linear/Vercel aesthetic
- **Subtle animations**: Fast (150-200ms), purposeful
- **Proper spacing**: Generous whitespace
- **Accessible**: Built on Radix UI primitives
+47 -32
View File
@@ -6,54 +6,69 @@
"serve": "vite preview",
"lint": "eslint --ext .js,.ts,.tsx, src",
"lint-fix": "eslint --ext .js,.ts,.tsx, src --fix",
"type-check": "tsc --noEmit --skipLibCheck",
"postinstall": "cd ../../proto && buf generate"
"type-check": "tsc --noEmit --skipLibCheck"
},
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@mui/joy": "5.0.0-beta.48",
"@reduxjs/toolkit": "^2.3.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@reduxjs/toolkit": "^2.8.2",
"class-variance-authority": "^0.7.1",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.13",
"i18next": "^23.16.4",
"framer-motion": "^12.23.26",
"i18next": "^24.2.3",
"lodash-es": "^4.17.21",
"lucide-react": "^0.446.0",
"nice-grpc-web": "^3.3.5",
"qrcode.react": "^4.1.0",
"lucide-react": "^0.469.0",
"next-themes": "^0.4.6",
"nice-grpc-web": "^3.3.7",
"qrcode.react": "^4.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
"react-i18next": "^15.1.0",
"react-router-dom": "^6.27.0",
"react-use": "^17.5.1",
"tailwindcss": "^3.4.14",
"uuid": "^10.0.0",
"zustand": "^5.0.1"
"react-i18next": "^15.6.1",
"react-router-dom": "^7.8.0",
"react-use": "^17.6.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^3.4.17",
"uuid": "^11.1.0",
"zustand": "^5.0.7"
},
"devDependencies": {
"@bufbuild/buf": "^1.46.0",
"@bufbuild/protobuf": "^2.2.2",
"@bufbuild/protobuf": "^2.6.3",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/node": "^25.0.3",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-react-swc": "^3.7.1",
"autoprefixer": "^10.4.20",
"@vitejs/plugin-react-swc": "^3.11.0",
"autoprefixer": "^10.4.21",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.2",
"long": "^5.2.3",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"protobufjs": "^7.4.0",
"typescript": "^5.6.3",
"vite": "^5.4.10"
"eslint-config-prettier": "^9.1.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"long": "^5.3.2",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"protobufjs": "^7.5.3",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.9.2",
"vite": "^6.3.5"
},
"resolutions": {
"csstype": "3.1.2"
+2629 -1638
View File
File diff suppressed because it is too large Load Diff
+5 -13
View File
@@ -1,14 +1,14 @@
import { useColorScheme } from "@mui/joy";
import { useTheme } from "next-themes";
import { useEffect } from "react";
import { Outlet } from "react-router-dom";
import DemoBanner from "@/components/DemoBanner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { useWorkspaceStore } from "@/stores";
import useNavigateTo from "./hooks/useNavigateTo";
import { FeatureType } from "./stores/workspace";
function App() {
const navigateTo = useNavigateTo();
const { mode: colorScheme } = useColorScheme();
const { theme: colorScheme } = useTheme();
const workspaceStore = useWorkspaceStore();
// Redirect to sign up page if no instance owner.
@@ -20,13 +20,6 @@ function App() {
}
}, [workspaceStore.profile]);
useEffect(() => {
const styleEl = document.createElement("style");
styleEl.innerHTML = workspaceStore.setting.customStyle;
styleEl.setAttribute("type", "text/css");
document.body.insertAdjacentElement("beforeend", styleEl);
}, [workspaceStore.setting.customStyle]);
useEffect(() => {
const hasCustomBranding = workspaceStore.checkFeatureAvailable(FeatureType.CustomeBranding);
if (!hasCustomBranding || !workspaceStore.setting.branding) {
@@ -71,10 +64,9 @@ function App() {
}, [colorScheme]);
return (
<>
<DemoBanner />
<TooltipProvider>
<Outlet />
</>
</TooltipProvider>
);
}
+17 -16
View File
@@ -1,6 +1,5 @@
import { Button, Link, Modal, ModalDialog } from "@mui/joy";
import { useTranslation } from "react-i18next";
import Icon from "./Icon";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
interface Props {
onClose: () => void;
@@ -11,28 +10,30 @@ const AboutDialog: React.FC<Props> = (props: Props) => {
const { t } = useTranslation();
return (
<Modal open={true}>
<ModalDialog>
<div className="w-full flex flex-row justify-between items-center">
<span className="text-lg font-medium">{t("common.about")}</span>
<Button variant="plain" onClick={onClose}>
<Icon.X className="w-5 h-auto text-gray-600" />
</Button>
</div>
<div className="max-w-full w-80 sm:w-96">
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="max-w-full w-80 sm:w-96">
<DialogHeader>
<DialogTitle>{t("common.about")}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p>
<span className="font-medium">Slash</span> is an open source, self-hosted platform for sharing and managing your most frequently
used links.
</p>
<div className="mt-1">
<div>
<span className="mr-2">Source code:</span>
<Link variant="plain" href="https://github.com/yourselfhosted/slash" target="_blank">
<a
className="text-primary hover:underline"
href="https://github.com/yourselfhosted/slash"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</Link>
</a>
</div>
</div>
</ModalDialog>
</Modal>
</DialogContent>
</Dialog>
);
};
+37 -31
View File
@@ -1,8 +1,16 @@
import { Button, Modal, ModalDialog } from "@mui/joy";
import { createRoot } from "react-dom/client";
import Icon from "./Icon";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
type AlertStyle = "primary" | "warning" | "danger";
type AlertStyle = "default" | "destructive" | "danger";
interface Props {
title: string;
@@ -14,10 +22,8 @@ interface Props {
onConfirm?: () => void;
}
const defaultProps: Props = {
title: "",
content: "",
style: "primary",
const defaultProps: Partial<Props> = {
style: "default",
closeBtnText: "Close",
confirmBtnText: "Confirm",
onClose: () => null,
@@ -43,27 +49,25 @@ const Alert: React.FC<Props> = (props: Props) => {
};
return (
<Modal open={true}>
<ModalDialog>
<div className="flex flex-row justify-between items-center w-80">
<span className="text-lg font-medium">{title}</span>
<Button variant="plain" onClick={handleCloseBtnClick}>
<Icon.X className="w-5 h-auto text-gray-600" />
</Button>
</div>
<div className="w-80">
<p className="content-text mb-4">{content}</p>
<div className="w-full flex flex-row justify-end items-center space-x-2">
<Button variant="plain" color="neutral" onClick={handleCloseBtnClick}>
{closeBtnText}
</Button>
<Button color={style} onClick={handleConfirmBtnClick}>
{confirmBtnText}
</Button>
</div>
</div>
</ModalDialog>
</Modal>
<AlertDialog open={true}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{content}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={handleCloseBtnClick}>{closeBtnText}</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmBtnClick}
className={
style === "destructive" || style === "danger" ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" : ""
}
>
{confirmBtnText}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -72,7 +76,7 @@ export const showCommonDialog = (props: Props) => {
const dialog = createRoot(tempDiv);
document.body.append(tempDiv);
const destory = () => {
const destroy = () => {
dialog.unmount();
tempDiv.remove();
};
@@ -81,15 +85,17 @@ export const showCommonDialog = (props: Props) => {
if (props.onClose) {
props.onClose();
}
destory();
destroy();
};
const onConfirm = () => {
if (props.onConfirm) {
props.onConfirm();
}
destory();
destroy();
};
dialog.render(<Alert {...props} onClose={onClose} onConfirm={onConfirm} />);
};
export default Alert;
+38 -32
View File
@@ -27,32 +27,34 @@ const AnalyticsView: React.FC<Props> = (props: Props) => {
{analytics ? (
<>
<div className="w-full">
<p className="w-full h-8 px-2 dark:text-gray-500">{t("analytics.top-sources")}</p>
<div className="w-full mt-1 overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg dark:ring-zinc-800">
<div className="w-full divide-y divide-gray-300 dark:divide-zinc-700">
<p className="w-full h-8 px-2 text-muted-foreground">{t("analytics.top-sources")}</p>
<div className="w-full mt-1 overflow-hidden shadow ring-1 ring-border rounded-lg">
<div className="w-full divide-y divide-border">
<div className="w-full flex flex-row justify-between items-center">
<span className="py-2 px-2 text-left font-semibold text-sm text-gray-500">{t("analytics.source")}</span>
<span className="py-2 pr-2 text-right font-semibold text-sm text-gray-500">{t("analytics.visitors")}</span>
<span className="py-2 px-2 text-left font-semibold text-sm text-muted-foreground">{t("analytics.source")}</span>
<span className="py-2 pr-2 text-right font-semibold text-sm text-muted-foreground">{t("analytics.visitors")}</span>
</div>
<div className="w-full divide-y divide-gray-200 dark:divide-zinc-800">
<div className="w-full divide-y divide-border">
{analytics.references.length === 0 && (
<div className="w-full flex flex-row justify-center items-center py-6 text-gray-400">
<div className="w-full flex flex-row justify-center items-center py-6 text-muted-foreground">
<Icon.PackageOpen className="w-6 h-auto" />
<p className="ml-2">No data found.</p>
</div>
)}
{analytics.references.map((reference) => (
<div key={reference.name} className="w-full flex flex-row justify-between items-center">
<span className="whitespace-nowrap py-2 px-2 text-sm truncate text-gray-900 dark:text-gray-500">
<span className="whitespace-nowrap py-2 px-2 text-sm truncate text-foreground">
{reference.name ? (
<a className="hover:underline hover:text-blue-600" href={reference.name} target="_blank">
<a className="hover:underline hover:text-primary" href={reference.name} target="_blank">
{reference.name}
</a>
) : (
"Direct"
)}
</span>
<span className="whitespace-nowrap py-2 pr-2 text-sm text-gray-500 text-right shrink-0">{reference.count}</span>
<span className="whitespace-nowrap py-2 pr-2 text-sm text-muted-foreground text-right shrink-0">
{reference.count}
</span>
</div>
))}
</div>
@@ -62,24 +64,24 @@ const AnalyticsView: React.FC<Props> = (props: Props) => {
<div className="w-full">
<div className="w-full h-8 px-2 flex flex-row justify-between items-center">
<span className="dark:text-gray-500">{t("analytics.devices")}</span>
<span className="text-muted-foreground">{t("analytics.devices")}</span>
<div>
<button
className={`whitespace-nowrap border-b-2 px-1 text-sm font-medium ${
selectedDeviceTab === "browser"
? "border-blue-600 text-blue-600"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:hover:border-zinc-700"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:border-border hover:text-foreground"
}`}
onClick={() => setSelectedDeviceTab("browser")}
>
{t("analytics.browser")}
</button>
<span className="text-gray-200 font-mono mx-1 dark:text-gray-500">/</span>
<span className="text-muted-foreground font-mono mx-1">/</span>
<button
className={`whitespace-nowrap border-b-2 px-1 text-sm font-medium ${
selectedDeviceTab === "os"
? "border-blue-600 text-blue-600"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:hover:border-zinc-700"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:border-border hover:text-foreground"
}`}
onClick={() => setSelectedDeviceTab("os")}
>
@@ -88,47 +90,51 @@ const AnalyticsView: React.FC<Props> = (props: Props) => {
</div>
</div>
<div className="w-full mt-1 overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg dark:ring-zinc-800">
<div className="w-full mt-1 overflow-hidden shadow ring-1 ring-border rounded-lg">
{selectedDeviceTab === "browser" ? (
<div className="w-full divide-y divide-gray-300 dark:divide-zinc-700">
<div className="w-full divide-y divide-border">
<div className="w-full flex flex-row justify-between items-center">
<span className="py-2 px-2 text-left text-sm font-semibold text-gray-500">{t("analytics.browsers")}</span>
<span className="py-2 pr-2 text-right text-sm font-semibold text-gray-500">{t("analytics.visitors")}</span>
<span className="py-2 px-2 text-left text-sm font-semibold text-muted-foreground">{t("analytics.browsers")}</span>
<span className="py-2 pr-2 text-right text-sm font-semibold text-muted-foreground">{t("analytics.visitors")}</span>
</div>
<div className="w-full divide-y divide-gray-200 dark:divide-zinc-800">
<div className="w-full divide-y divide-border">
{analytics.browsers.length === 0 && (
<div className="w-full flex flex-row justify-center items-center py-6 text-gray-400">
<div className="w-full flex flex-row justify-center items-center py-6 text-muted-foreground">
<Icon.PackageOpen className="w-6 h-auto" />
<p className="ml-2">No data found.</p>
</div>
)}
{analytics.browsers.map((reference) => (
<div key={reference.name} className="w-full flex flex-row justify-between items-center">
<span className="whitespace-nowrap py-2 px-2 text-sm text-gray-900 truncate dark:text-gray-500">
{reference.name || "Unknown"}
<span className="whitespace-nowrap py-2 px-2 text-sm text-foreground truncate">{reference.name || "Unknown"}</span>
<span className="whitespace-nowrap py-2 pr-2 text-sm text-muted-foreground text-right shrink-0">
{reference.count}
</span>
<span className="whitespace-nowrap py-2 pr-2 text-sm text-gray-500 text-right shrink-0">{reference.count}</span>
</div>
))}
</div>
</div>
) : (
<div className="w-full divide-y divide-gray-300">
<div className="w-full divide-y divide-border">
<div className="w-full flex flex-row justify-between items-center">
<span className="py-2 px-2 text-left text-sm font-semibold text-gray-500">{t("analytics.operating-system")}</span>
<span className="py-2 pr-2 text-right text-sm font-semibold text-gray-500">{t("analytics.visitors")}</span>
<span className="py-2 px-2 text-left text-sm font-semibold text-muted-foreground">
{t("analytics.operating-system")}
</span>
<span className="py-2 pr-2 text-right text-sm font-semibold text-muted-foreground">{t("analytics.visitors")}</span>
</div>
<div className="w-full divide-y divide-gray-200">
<div className="w-full divide-y divide-border">
{analytics.devices.length === 0 && (
<div className="w-full flex flex-row justify-center items-center py-6 text-gray-400">
<div className="w-full flex flex-row justify-center items-center py-6 text-muted-foreground">
<Icon.PackageOpen className="w-6 h-auto" />
<p className="ml-2">No data found.</p>
</div>
)}
{analytics.devices.map((device) => (
<div key={device.name} className="w-full flex flex-row justify-between items-center">
<span className="whitespace-nowrap py-2 px-2 text-sm text-gray-900 truncate">{device.name || "Unknown"}</span>
<span className="whitespace-nowrap py-2 pr-2 text-sm text-gray-500 text-right shrink-0">{device.count}</span>
<span className="whitespace-nowrap py-2 px-2 text-sm text-foreground truncate">{device.name || "Unknown"}</span>
<span className="whitespace-nowrap py-2 pr-2 text-sm text-muted-foreground text-right shrink-0">
{device.count}
</span>
</div>
))}
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
const BetaBadge = () => {
return (
<div className="text-xs border px-1 text-gray-500 bg-gray-100 rounded-full dark:bg-zinc-800 dark:border-zinc-700">
<div className="text-xs border border-border px-1 text-muted-foreground bg-muted rounded-full">
<span>Beta</span>
</div>
);
@@ -1,10 +1,12 @@
import { Button, Input, Modal, ModalDialog } from "@mui/joy";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import useLoading from "@/hooks/useLoading";
import { useUserStore } from "@/stores";
import Icon from "./Icon";
interface Props {
onClose: () => void;
@@ -18,10 +20,6 @@ const ChangePasswordDialog: React.FC<Props> = (props: Props) => {
const [newPasswordAgain, setNewPasswordAgain] = useState("");
const requestState = useLoading(false);
const handleCloseBtnClick = () => {
onClose();
};
const handleNewPasswordChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value as string;
setNewPassword(text);
@@ -54,7 +52,7 @@ const ChangePasswordDialog: React.FC<Props> = (props: Props) => {
["password"],
);
onClose();
toast("Password changed");
toast.success("Password changed");
} catch (error: any) {
console.error(error);
toast.error(error.details);
@@ -63,34 +61,31 @@ const ChangePasswordDialog: React.FC<Props> = (props: Props) => {
};
return (
<Modal open={true}>
<ModalDialog>
<div className="flex flex-row justify-between items-center w-80">
<span className="text-lg font-medium">Change Password</span>
<Button variant="plain" onClick={handleCloseBtnClick}>
<Icon.X className="w-5 h-auto text-gray-600" />
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="w-80 sm:max-w-md">
<DialogHeader>
<DialogTitle>Change Password</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="new-password">New Password</Label>
<Input id="new-password" type="password" value={newPassword} onChange={handleNewPasswordChanged} />
</div>
<div className="space-y-2">
<Label htmlFor="new-password-again">New Password Again</Label>
<Input id="new-password-again" type="password" value={newPasswordAgain} onChange={handleNewPasswordAgainChanged} />
</div>
</div>
<DialogFooter>
<Button variant="outline" disabled={requestState.isLoading} onClick={onClose}>
{t("common.cancel")}
</Button>
</div>
<div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">New Password</span>
<Input className="w-full" type="text" value={newPassword} onChange={handleNewPasswordChanged} />
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">New Password Again</span>
<Input className="w-full" type="text" value={newPasswordAgain} onChange={handleNewPasswordAgainChanged} />
</div>
<div className="w-full flex flex-row justify-end items-center space-x-2">
<Button variant="plain" disabled={requestState.isLoading} onClick={handleCloseBtnClick}>
{t("common.cancel")}
</Button>
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={handleSaveBtnClick}>
{t("common.save")}
</Button>
</div>
</div>
</ModalDialog>
</Modal>
<Button disabled={requestState.isLoading} onClick={handleSaveBtnClick}>
{requestState.isLoading ? "Saving..." : t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
+31 -21
View File
@@ -1,10 +1,10 @@
import { Tooltip } from "@mui/joy";
import classNames from "classnames";
import copy from "copy-to-clipboard";
import { useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { absolutifyLink } from "@/helpers/utils";
import useNavigateTo from "@/hooks/useNavigateTo";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
@@ -62,37 +62,47 @@ const CollectionView = (props: Props) => {
return (
<>
<div className={classNames("w-full flex flex-col justify-start items-start border rounded-lg hover:shadow dark:border-zinc-800")}>
<div className="bg-gray-100 dark:bg-zinc-800 px-3 py-2 w-full flex flex-row justify-between items-center rounded-t-lg">
<div className={classNames("w-full flex flex-col justify-start items-start border border-border rounded-lg hover:shadow")}>
<div className="bg-muted px-3 py-2 w-full flex flex-row justify-between items-center rounded-t-lg">
<div className="w-auto flex flex-col justify-start items-start mr-2">
<div className="w-full truncate">
<Link className="leading-6 font-medium dark:text-gray-400" to={`/c/${collection.name}`} viewTransition>
<Link className="leading-6 font-medium text-foreground" to={`/c/${collection.name}`} viewTransition>
{collection.title}
</Link>
<span className="ml-1 leading-6 text-gray-500 dark:text-gray-400" onClick={handleCopyCollectionLink}>
<span className="ml-1 leading-6 text-muted-foreground" onClick={handleCopyCollectionLink}>
(c/{collection.name})
</span>
</div>
<p className="text-sm text-gray-500">{collection.description}</p>
<p className="text-sm text-muted-foreground">{collection.description}</p>
</div>
<div className="flex flex-row justify-end items-center shrink-0 gap-2">
<Tooltip title="Share" placement="top" arrow>
<Link className="w-auto text-gray-400 cursor-pointer hover:text-gray-500" to={`/c/${collection.name}`} target="_blank">
<Icon.Share className="w-4 h-auto" />
</Link>
<Tooltip>
<TooltipTrigger asChild>
<Link
className="w-auto text-muted-foreground cursor-pointer hover:text-foreground"
to={`/c/${collection.name}`}
target="_blank"
>
<Icon.Share className="w-4 h-auto" />
</Link>
</TooltipTrigger>
<TooltipContent>Share</TooltipContent>
</Tooltip>
<Tooltip title="Open all" placement="top" arrow>
<button
className="w-auto text-gray-400 cursor-pointer hover:text-gray-500"
onClick={() => handleOpenAllShortcutsButtonClick()}
>
<Icon.ArrowUpRight className="w-5 h-auto" />
</button>
<Tooltip>
<TooltipTrigger asChild>
<button
className="w-auto text-muted-foreground cursor-pointer hover:text-foreground"
onClick={() => handleOpenAllShortcutsButtonClick()}
>
<Icon.ArrowUpRight className="w-5 h-auto" />
</button>
</TooltipTrigger>
<TooltipContent>Open all</TooltipContent>
</Tooltip>
{showAdminActions && (
<Dropdown
trigger={
<button className="flex flex-row justify-center items-center rounded text-gray-400 cursor-pointer hover:text-gray-500">
<button className="flex flex-row justify-center items-center rounded text-muted-foreground cursor-pointer hover:text-foreground">
<Icon.MoreVertical className="w-4 h-auto" />
</button>
}
@@ -100,13 +110,13 @@ const CollectionView = (props: Props) => {
actions={
<>
<button
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
className="w-full px-2 flex flex-row justify-start items-center text-left text-foreground leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
onClick={() => setShowEditDialog(true)}
>
<Icon.Edit className="w-4 h-auto mr-2" /> {t("common.edit")}
</button>
<button
className="w-full px-2 flex flex-row justify-start items-center text-left text-red-600 dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
className="w-full px-2 flex flex-row justify-start items-center text-left text-destructive leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
onClick={() => {
handleDeleteCollectionButtonClick();
}}
@@ -1,11 +1,14 @@
import { Button, Input, Modal, ModalDialog, Radio, RadioGroup } from "@mui/joy";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { userServiceClient } from "@/grpcweb";
import useLoading from "@/hooks/useLoading";
import { useUserStore } from "@/stores";
import Icon from "./Icon";
interface Props {
onClose: () => void;
@@ -55,9 +58,9 @@ const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
});
};
const handleRoleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleExpirationChange = (value: string) => {
setPartialState({
expiration: Number(e.target.value),
expiration: Number(value),
});
};
@@ -85,52 +88,52 @@ const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
};
return (
<Modal open={true}>
<ModalDialog>
<div className="flex flex-row justify-between items-center w-80">
<span className="text-lg font-medium">Create Access Token</span>
<Button variant="plain" onClick={onClose}>
<Icon.X className="w-5 h-auto text-gray-600" />
</Button>
</div>
<div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Description <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<Input
className="w-full"
type="text"
placeholder="Some description"
value={state.description}
onChange={handleDescriptionInputChange}
/>
</div>
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="w-80 sm:max-w-md">
<DialogHeader>
<DialogTitle>Create Access Token</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="description">
Description <span className="text-destructive">*</span>
</Label>
<Input
id="description"
type="text"
placeholder="Some description"
value={state.description}
onChange={handleDescriptionInputChange}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Expiration <span className="text-red-600">*</span>
</span>
<div className="w-full flex flex-row justify-start items-center text-base">
<RadioGroup orientation="horizontal" value={state.expiration} onChange={handleRoleInputChange}>
<div className="space-y-2">
<Label>
Expiration <span className="text-destructive">*</span>
</Label>
<RadioGroup value={String(state.expiration)} onValueChange={handleExpirationChange}>
<div className="flex flex-col space-y-2">
{expirationOptions.map((option) => (
<Radio key={option.value} value={option.value} checked={state.expiration === option.value} label={option.label} />
<div key={option.value} className="flex items-center space-x-2">
<RadioGroupItem value={String(option.value)} id={`expiration-${option.value}`} />
<Label htmlFor={`expiration-${option.value}`} className="font-normal cursor-pointer">
{option.label}
</Label>
</div>
))}
</RadioGroup>
</div>
</div>
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
<Button color="neutral" variant="plain" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onClose}>
{t("common.cancel")}
</Button>
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={handleSaveBtnClick}>
{t("common.create")}
</Button>
</div>
</RadioGroup>
</div>
</div>
</ModalDialog>
</Modal>
<DialogFooter>
<Button variant="outline" disabled={requestState.isLoading} onClick={onClose}>
{t("common.cancel")}
</Button>
<Button disabled={requestState.isLoading} onClick={handleSaveBtnClick}>
{requestState.isLoading ? "Creating..." : t("common.create")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -1,8 +1,13 @@
import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, Divider, Drawer, Input, ModalClose } from "@mui/joy";
import { isUndefined } from "lodash-es";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import useLoading from "@/hooks/useLoading";
import { useCollectionStore, useShortcutStore, useWorkspaceStore } from "@/stores";
import { Collection } from "@/types/proto/api/v1/collection_service";
@@ -157,117 +162,118 @@ const CreateCollectionDrawer: React.FC<Props> = (props: Props) => {
};
return (
<Drawer anchor="right" open={true} onClose={onClose}>
<DialogTitle>{isCreating ? "Create Collection" : "Edit Collection"}</DialogTitle>
<ModalClose />
<DialogContent className="w-full max-w-full">
<div className="overflow-y-auto w-full mt-2 px-4 pb-4 sm:w-[24rem]">
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Name <span className="text-red-600">*</span>
</span>
<Input
className="w-full"
type="text"
startDecorator="c/"
placeholder="An easy name to remember"
value={state.collectionCreate.name}
onChange={handleNameInputChange}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Title <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<Sheet open={true} onOpenChange={onClose}>
<SheetContent className="w-full sm:max-w-md">
<SheetHeader>
<SheetTitle>{isCreating ? "Create Collection" : "Edit Collection"}</SheetTitle>
</SheetHeader>
<SheetBody>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">
Name <span className="text-destructive">*</span>
</Label>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">c/</span>
<Input
id="name"
type="text"
placeholder="An easy name to remember"
value={state.collectionCreate.name}
onChange={handleNameInputChange}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="title">
Title <span className="text-destructive">*</span>
</Label>
<Input
className="w-full"
id="title"
type="text"
placeholder="A short title of your collection"
value={state.collectionCreate.title}
onChange={handleTitleInputChange}
/>
</div>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">Description</span>
<div className="relative w-full">
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Input
className="w-full"
id="description"
type="text"
placeholder="A slightly longer description"
value={state.collectionCreate.description}
onChange={handleDescriptionInputChange}
/>
</div>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<Checkbox
className="w-full dark:text-gray-400"
checked={state.collectionCreate.visibility === Visibility.PUBLIC}
label={t(`shortcut.visibility.public.description`)}
onChange={(e) =>
setPartialState({
collectionCreate: Object.assign(state.collectionCreate, {
visibility: e.target.checked ? Visibility.PUBLIC : Visibility.WORKSPACE,
}),
})
}
/>
</div>
<Divider className="text-gray-500" />
<div className="w-full flex flex-col justify-start items-start mt-3 mb-3">
<p className="mb-2">
<span>Shortcuts</span>
<span className="opacity-60">({selectedShortcuts.length})</span>
{selectedShortcuts.length === 0 && <span className="ml-2 italic opacity-80 text-sm">(Select a shortcut first)</span>}
</p>
<div className="w-full py-1 px-px flex flex-row justify-start items-start flex-wrap overflow-hidden gap-2">
{selectedShortcuts.map((shortcut) => {
return (
<ShortcutView
key={shortcut.id}
className="!w-auto select-none max-w-[40%] cursor-pointer bg-gray-100 shadow dark:bg-zinc-800 dark:border-zinc-700 dark:text-gray-400"
shortcut={shortcut}
onClick={() => {
setSelectedShortcuts([...selectedShortcuts.filter((selectedShortcut) => selectedShortcut.id !== shortcut.id)]);
}}
/>
);
})}
{unselectedShortcuts.map((shortcut) => {
return (
<ShortcutView
key={shortcut.id}
className="!w-auto select-none max-w-[40%] border-dashed cursor-pointer"
shortcut={shortcut}
onClick={() => {
setSelectedShortcuts([...selectedShortcuts, shortcut]);
}}
/>
);
})}
{selectedShortcuts.length + unselectedShortcuts.length === 0 && (
<div className="w-full flex flex-row justify-center items-center text-gray-400">
<Icon.PackageOpen className="w-6 h-auto" />
<p className="ml-2">No shortcuts found.</p>
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="public"
checked={state.collectionCreate.visibility === Visibility.PUBLIC}
onCheckedChange={(checked) =>
setPartialState({
collectionCreate: Object.assign(state.collectionCreate, {
visibility: checked ? Visibility.PUBLIC : Visibility.WORKSPACE,
}),
})
}
/>
<Label htmlFor="public" className="text-sm font-normal cursor-pointer">
{t(`shortcut.visibility.public.description`)}
</Label>
</div>
<Separator />
<div className="space-y-2">
<div className="flex items-baseline gap-2">
<Label>Shortcuts</Label>
<span className="text-sm text-muted-foreground">({selectedShortcuts.length})</span>
{selectedShortcuts.length === 0 && <span className="text-sm italic text-muted-foreground">(Select a shortcut first)</span>}
</div>
<div className="w-full py-1 px-px flex flex-row justify-start items-start flex-wrap gap-2">
{selectedShortcuts.map((shortcut) => {
return (
<ShortcutView
key={shortcut.id}
className="!w-auto select-none max-w-[40%] cursor-pointer bg-muted shadow"
shortcut={shortcut}
onClick={() => {
setSelectedShortcuts([...selectedShortcuts.filter((selectedShortcut) => selectedShortcut.id !== shortcut.id)]);
}}
/>
);
})}
{unselectedShortcuts.map((shortcut) => {
return (
<ShortcutView
key={shortcut.id}
className="!w-auto select-none max-w-[40%] border-dashed cursor-pointer"
shortcut={shortcut}
onClick={() => {
setSelectedShortcuts([...selectedShortcuts, shortcut]);
}}
/>
);
})}
{selectedShortcuts.length + unselectedShortcuts.length === 0 && (
<div className="w-full flex flex-row justify-center items-center text-muted-foreground">
<Icon.PackageOpen className="w-6 h-auto" />
<p className="ml-2">No shortcuts found.</p>
</div>
)}
</div>
</div>
</div>
</div>
</DialogContent>
<DialogActions>
<div className="w-full flex flex-row justify-end items-center px-3 py-4 space-x-2">
<Button color="neutral" variant="plain" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onClose}>
</SheetBody>
<SheetFooter>
<Button variant="outline" disabled={requestState.isLoading} onClick={onClose}>
{t("common.cancel")}
</Button>
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={handleSaveBtnClick}>
{t("common.save")}
<Button disabled={requestState.isLoading} onClick={handleSaveBtnClick}>
{requestState.isLoading ? "Saving..." : t("common.save")}
</Button>
</div>
</DialogActions>
</Drawer>
</SheetFooter>
</SheetContent>
</Sheet>
);
};
@@ -1,9 +1,13 @@
import { Button, DialogActions, DialogContent, DialogTitle, Divider, Drawer, Input, ModalClose } from "@mui/joy";
import { isUndefined } from "lodash-es";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { v4 as uuidv4 } from "uuid";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { workspaceServiceClient } from "@/grpcweb";
import { absolutifyLink } from "@/helpers/utils";
import useLoading from "@/hooks/useLoading";
@@ -132,139 +136,128 @@ const CreateIdentityProviderDrawer: React.FC<Props> = (props: Props) => {
};
return (
<Drawer anchor="right" open={true} onClose={onClose}>
<DialogTitle>{isCreating ? "Create Identity Provider" : "Edit Identity Provider"}</DialogTitle>
<ModalClose />
<DialogContent className="w-full max-w-full">
<div className="overflow-y-auto w-full mt-2 px-4 pb-4 sm:w-[24rem]">
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Title <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<Sheet open={true} onOpenChange={onClose}>
<SheetContent className="w-full sm:max-w-md">
<SheetHeader>
<SheetTitle>{isCreating ? "Create Identity Provider" : "Edit Identity Provider"}</SheetTitle>
</SheetHeader>
<SheetBody>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">
Title <span className="text-destructive">*</span>
</Label>
<Input
className="w-full"
id="title"
type="text"
placeholder="A short title will be displayed in the UI"
value={state.identityProviderCreate.title}
onChange={handleTitleInputChange}
/>
</div>
</div>
<Divider className="!mb-3" />
<p className="font-medium mb-2">Identity provider information</p>
{isCreating && (
<p className="shadow-sm rounded-md py-1 px-2 bg-zinc-100 dark:bg-zinc-900 text-sm w-full mb-2 break-all">
<span className="opacity-60">Redirect URL</span>
<br />
<code>{absolutifyLink("/auth/callback")}</code>
</p>
)}
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Client ID <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<Separator />
<div className="space-y-2">
<p className="text-sm font-medium">Identity provider information</p>
{isCreating && (
<div className="rounded-md py-2 px-3 bg-secondary text-sm w-full break-all">
<span className="text-muted-foreground">Redirect URL</span>
<br />
<code className="text-xs">{absolutifyLink("/auth/callback")}</code>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="client-id">
Client ID <span className="text-destructive">*</span>
</Label>
<Input
className="w-full"
id="client-id"
type="text"
placeholder="Client ID of the OAuth2 provider"
value={state.identityProviderCreate.config?.oauth2?.clientId}
onChange={(e) => handleOAuth2ConfigChange(e, "clientId")}
/>
</div>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Client Secret <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<div className="space-y-2">
<Label htmlFor="client-secret">
Client Secret <span className="text-destructive">*</span>
</Label>
<Input
className="w-full"
id="client-secret"
type="text"
placeholder="Client Secret of the OAuth2 provider"
value={state.identityProviderCreate.config?.oauth2?.clientSecret}
onChange={(e) => handleOAuth2ConfigChange(e, "clientSecret")}
/>
</div>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Authorization endpoint <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<div className="space-y-2">
<Label htmlFor="auth-url">
Authorization endpoint <span className="text-destructive">*</span>
</Label>
<Input
className="w-full"
id="auth-url"
type="text"
placeholder="Authorization endpoint of the OAuth2 provider"
value={state.identityProviderCreate.config?.oauth2?.authUrl}
onChange={(e) => handleOAuth2ConfigChange(e, "authUrl")}
/>
</div>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Token endpoint <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<div className="space-y-2">
<Label htmlFor="token-url">
Token endpoint <span className="text-destructive">*</span>
</Label>
<Input
className="w-full"
id="token-url"
type="text"
placeholder="Token endpoint of the OAuth2 provider"
value={state.identityProviderCreate.config?.oauth2?.tokenUrl}
onChange={(e) => handleOAuth2ConfigChange(e, "tokenUrl")}
/>
</div>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
User endpoint <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<div className="space-y-2">
<Label htmlFor="user-info-url">
User endpoint <span className="text-destructive">*</span>
</Label>
<Input
className="w-full"
id="user-info-url"
type="text"
placeholder="User endpoint of the OAuth2 provider"
value={state.identityProviderCreate.config?.oauth2?.userInfoUrl}
onChange={(e) => handleOAuth2ConfigChange(e, "userInfoUrl")}
/>
</div>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Scopes <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<div className="space-y-2">
<Label htmlFor="scopes">
Scopes <span className="text-destructive">*</span>
</Label>
<Input
className="w-full"
id="scopes"
type="text"
placeholder="Scopes of the OAuth2 provider, separated by space"
value={state.identityProviderCreate.config?.oauth2?.scopes.join(" ")}
onChange={(e) => handleOAuth2ConfigChange(e, "scopes")}
/>
</div>
</div>
<Divider className="!mb-3" />
<p className="font-medium mb-2">Field mapping</p>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Identifier <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<Separator />
<div className="space-y-2">
<p className="text-sm font-medium">Field mapping</p>
</div>
<div className="space-y-2">
<Label htmlFor="identifier">
Identifier <span className="text-destructive">*</span>
</Label>
<Input
className="w-full"
id="identifier"
type="text"
placeholder="The field in the user info response to identify the user"
value={state.identityProviderCreate.config?.oauth2?.fieldMapping?.identifier}
onChange={(e) => handleFieldMappingChange(e, "identifier")}
/>
</div>
</div>
<div className="w-full flex flex-col justify-start items-start">
<span className="mb-2">Display name</span>
<div className="relative w-full">
<div className="space-y-2">
<Label htmlFor="display-name">Display name</Label>
<Input
className="w-full"
id="display-name"
type="text"
placeholder="The field in the user info response to display the user"
value={state.identityProviderCreate.config?.oauth2?.fieldMapping?.displayName}
@@ -272,19 +265,17 @@ const CreateIdentityProviderDrawer: React.FC<Props> = (props: Props) => {
/>
</div>
</div>
</div>
</DialogContent>
<DialogActions>
<div className="w-full flex flex-row justify-end items-center px-3 py-4 space-x-2">
<Button color="neutral" variant="plain" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onClose}>
</SheetBody>
<SheetFooter>
<Button variant="outline" disabled={requestState.isLoading} onClick={onClose}>
{t("common.cancel")}
</Button>
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onSave}>
{t("common.save")}
<Button disabled={requestState.isLoading} onClick={onSave}>
{requestState.isLoading ? "Saving..." : t("common.save")}
</Button>
</div>
</DialogActions>
</Drawer>
</SheetFooter>
</SheetContent>
</Sheet>
);
};
@@ -1,9 +1,15 @@
import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, Divider, Drawer, Input, ModalClose, Textarea } from "@mui/joy";
import classnames from "classnames";
import { isUndefined, uniq } from "lodash-es";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { Textarea } from "@/components/ui/textarea";
import useLoading from "@/hooks/useLoading";
import { useShortcutStore, useWorkspaceStore } from "@/stores";
import { getShortcutUpdateMask } from "@/stores/shortcut";
@@ -173,6 +179,7 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
}
try {
requestState.setLoading();
const tags = tag.split(" ").filter(Boolean);
if (shortcutId) {
const originShortcut = shortcutStore.getShortcutById(shortcutId);
@@ -189,6 +196,7 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
});
}
requestState.setFinish();
if (onConfirm) {
onConfirm();
} else {
@@ -196,163 +204,172 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
}
} catch (error: any) {
console.error(error);
toast.error(error.details);
toast.error(error.details || "An error occurred");
requestState.setFinish();
}
};
return (
<Drawer anchor="right" open={true} onClose={onClose}>
<DialogTitle>{isCreating ? "Create Shortcut" : "Edit Shortcut"}</DialogTitle>
<ModalClose />
<DialogContent className="w-full max-w-full">
<div className="overflow-y-auto w-full mt-2 px-4 pb-4 sm:w-[24rem]">
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Name <span className="text-red-600">*</span>
</span>
<Input
className="w-full"
type="text"
startDecorator="s/"
placeholder="An easy name to remember"
value={state.shortcutCreate.name}
onChange={handleNameInputChange}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Link <span className="text-red-600">*</span>
</span>
<Input
className="w-full"
type="text"
placeholder="The destination link of the shortcut"
value={state.shortcutCreate.link}
onChange={handleLinkInputChange}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">Title</span>
<Input
className="w-full"
type="text"
placeholder="The title of the shortcut"
value={state.shortcutCreate.title}
onChange={handleTitleInputChange}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">Description</span>
<Input
className="w-full"
type="text"
placeholder="A short description of the shortcut"
value={state.shortcutCreate.description}
onChange={handleDescriptionInputChange}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">Tags</span>
<Input className="w-full" type="text" placeholder="The tags of shortcut" value={tag} onChange={handleTagsInputChange} />
{tagSuggestions.length > 0 && (
<div className="w-full flex flex-row justify-start items-start mt-2">
<Icon.Asterisk className="w-4 h-auto shrink-0 mx-1 text-gray-400 dark:text-gray-500" />
<div className="w-auto flex flex-row justify-start items-start flex-wrap gap-x-2 gap-y-1">
{tagSuggestions.map((tag) => (
<span
className="text-gray-600 dark:text-gray-500 cursor-pointer max-w-[6rem] truncate block text-sm flex-nowrap leading-4 hover:text-black dark:hover:text-gray-400"
key={tag}
onClick={() => handleTagSuggestionsClick(tag)}
>
{tag}
</span>
))}
</div>
<Sheet open={true} onOpenChange={onClose}>
<SheetContent className="w-full sm:max-w-md">
<SheetHeader>
<SheetTitle>{isCreating ? "Create Shortcut" : "Edit Shortcut"}</SheetTitle>
</SheetHeader>
<SheetBody>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">
Name <span className="text-destructive">*</span>
</Label>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">s/</span>
<Input
id="name"
type="text"
placeholder="An easy name to remember"
value={state.shortcutCreate.name}
onChange={handleNameInputChange}
/>
</div>
)}
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<Checkbox
className="w-full dark:text-gray-400"
checked={state.shortcutCreate.visibility === Visibility.PUBLIC}
label={t(`shortcut.visibility.public.description`)}
onChange={(e) =>
setPartialState({
shortcutCreate: Object.assign(state.shortcutCreate, {
visibility: e.target.checked ? Visibility.PUBLIC : Visibility.WORKSPACE,
}),
})
}
/>
</div>
<Divider className="text-gray-500">More</Divider>
<div className="w-full flex flex-col justify-start items-start border rounded-md mt-3 overflow-hidden dark:border-zinc-800">
<div
className={classnames(
"w-full flex flex-row justify-between items-center px-2 py-1 cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-800",
showOpenGraphMetadata ? "bg-gray-100 border-b dark:bg-zinc-800 dark:border-b-zinc-700" : "",
)}
onClick={() => setShowOpenGraphMetadata(!showOpenGraphMetadata)}
>
<span className="text-sm flex flex-row justify-start items-center">
Social media metadata
<Icon.Sparkles className="w-4 h-auto shrink-0 ml-1 text-blue-600 dark:text-blue-500" />
</span>
<button className="w-7 h-7 p-1 rounded-md">
<Icon.ChevronDown className={classnames("w-4 h-auto text-gray-500", showOpenGraphMetadata ? "transform rotate-180" : "")} />
</button>
</div>
{showOpenGraphMetadata && (
<div className="w-full px-2 py-1">
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2 text-sm">Image URL</span>
<Input
className="w-full"
type="text"
placeholder="https://the.link.to/the/image.png"
size="sm"
value={state.shortcutCreate.ogMetadata?.image}
onChange={handleOpenGraphMetadataImageChange}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2 text-sm">Title</span>
<Input
className="w-full"
type="text"
placeholder="Slash - An open source, self-hosted platform for sharing and managing your most frequently used links"
size="sm"
value={state.shortcutCreate.ogMetadata?.title}
onChange={handleOpenGraphMetadataTitleChange}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2 text-sm">Description</span>
<Textarea
className="w-full"
placeholder="An open source, self-hosted platform for sharing and managing your most frequently used links."
size="sm"
maxRows={3}
value={state.shortcutCreate.ogMetadata?.description}
onChange={handleOpenGraphMetadataDescriptionChange}
/>
<div className="space-y-2">
<Label htmlFor="link">
Link <span className="text-destructive">*</span>
</Label>
<Input
id="link"
type="text"
placeholder="The destination link of the shortcut"
value={state.shortcutCreate.link}
onChange={handleLinkInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="title">Title</Label>
<Input
id="title"
type="text"
placeholder="The title of the shortcut"
value={state.shortcutCreate.title}
onChange={handleTitleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
type="text"
placeholder="A short description of the shortcut"
value={state.shortcutCreate.description}
onChange={handleDescriptionInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="tags">Tags</Label>
<Input id="tags" type="text" placeholder="The tags of shortcut" value={tag} onChange={handleTagsInputChange} />
{tagSuggestions.length > 0 && (
<div className="flex flex-row items-start gap-2 mt-2">
<Icon.Asterisk className="w-4 h-auto shrink-0 mt-0.5 text-muted-foreground" />
<div className="flex flex-row flex-wrap gap-2">
{tagSuggestions.map((tag) => (
<span
className="text-muted-foreground cursor-pointer text-sm hover:text-foreground transition-colors"
key={tag}
onClick={() => handleTagSuggestionsClick(tag)}
>
{tag}
</span>
))}
</div>
</div>
)}
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="public"
checked={state.shortcutCreate.visibility === Visibility.PUBLIC}
onCheckedChange={(checked) =>
setPartialState({
shortcutCreate: Object.assign(state.shortcutCreate, {
visibility: checked ? Visibility.PUBLIC : Visibility.WORKSPACE,
}),
})
}
/>
<Label htmlFor="public" className="text-sm font-normal cursor-pointer">
{t(`shortcut.visibility.public.description`)}
</Label>
</div>
<Separator className="my-4" />
<div className="border rounded-lg overflow-hidden">
<div
className={classnames(
"flex flex-row justify-between items-center px-3 py-2 cursor-pointer hover:bg-accent transition-colors",
showOpenGraphMetadata && "bg-accent border-b",
)}
onClick={() => setShowOpenGraphMetadata(!showOpenGraphMetadata)}
>
<span className="text-sm flex items-center gap-1">
Social media metadata
<Icon.Sparkles className="w-4 h-auto text-primary" />
</span>
<Icon.ChevronDown
className={classnames("w-4 h-auto text-muted-foreground transition-transform", showOpenGraphMetadata && "rotate-180")}
/>
</div>
)}
{showOpenGraphMetadata && (
<div className="p-3 space-y-4">
<div className="space-y-2">
<Label htmlFor="og-image" className="text-sm">
Image URL
</Label>
<Input
id="og-image"
type="text"
placeholder="https://the.link.to/the/image.png"
value={state.shortcutCreate.ogMetadata?.image}
onChange={handleOpenGraphMetadataImageChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="og-title" className="text-sm">
Title
</Label>
<Input
id="og-title"
type="text"
placeholder="Slash - An open source, self-hosted platform"
value={state.shortcutCreate.ogMetadata?.title}
onChange={handleOpenGraphMetadataTitleChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="og-description" className="text-sm">
Description
</Label>
<Textarea
id="og-description"
placeholder="An open source, self-hosted platform for sharing and managing your most frequently used links."
rows={3}
value={state.shortcutCreate.ogMetadata?.description}
onChange={handleOpenGraphMetadataDescriptionChange}
/>
</div>
</div>
)}
</div>
</div>
</div>
</DialogContent>
<DialogActions>
<div className="w-full flex flex-row justify-end items-center px-3 py-4 space-x-2">
<Button color="neutral" variant="plain" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onClose}>
</SheetBody>
<SheetFooter>
<Button variant="outline" onClick={onClose} disabled={requestState.isLoading}>
{t("common.cancel")}
</Button>
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={handleSaveBtnClick}>
{t("common.save")}
<Button onClick={handleSaveBtnClick} disabled={requestState.isLoading}>
{requestState.isLoading ? "Saving..." : t("common.save")}
</Button>
</div>
</DialogActions>
</Drawer>
</SheetFooter>
</SheetContent>
</Sheet>
);
};
@@ -1,12 +1,15 @@
import { Button, Input, Modal, ModalDialog, Radio, RadioGroup } from "@mui/joy";
import { isUndefined } from "lodash-es";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import useLoading from "@/hooks/useLoading";
import { useUserStore } from "@/stores";
import { Role, User } from "@/types/proto/api/v1/user_service";
import Icon from "./Icon";
import { Role, type User } from "@/types/proto/api/v1/user_service";
interface Props {
user?: User;
@@ -78,10 +81,10 @@ const CreateUserDialog: React.FC<Props> = (props: Props) => {
});
};
const handleRoleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleRoleChange = (value: string) => {
setPartialState({
userCreate: Object.assign(state.userCreate, {
role: e.target.value,
role: value,
}),
});
};
@@ -127,35 +130,30 @@ const CreateUserDialog: React.FC<Props> = (props: Props) => {
};
return (
<Modal open={true}>
<ModalDialog>
<div className="flex flex-row justify-between items-center w-80 sm:w-96">
<span className="text-lg font-medium">{isCreating ? "Create User" : "Edit User"}</span>
<Button variant="plain" onClick={onClose}>
<Icon.X className="w-5 h-auto text-gray-600" />
</Button>
</div>
<div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Email <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<Input
className="w-full"
type="email"
placeholder="Unique user email"
value={state.userCreate.email}
onChange={handleEmailInputChange}
/>
</div>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Nickname <span className="text-red-600">*</span>
</span>
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="w-80 sm:w-96">
<DialogHeader>
<DialogTitle>{isCreating ? "Create User" : "Edit User"}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">
Email <span className="text-destructive">*</span>
</Label>
<Input
className="w-full"
id="email"
type="email"
placeholder="Unique user email"
value={state.userCreate.email}
onChange={handleEmailInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="nickname">
Nickname <span className="text-destructive">*</span>
</Label>
<Input
id="nickname"
type="text"
placeholder="Nickname"
value={state.userCreate.nickname}
@@ -163,41 +161,45 @@ const CreateUserDialog: React.FC<Props> = (props: Props) => {
/>
</div>
{isCreating && (
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Password <span className="text-red-600">*</span>
</span>
<Input
className="w-full"
type="password"
placeholder=""
value={state.userCreate.password}
onChange={handlePasswordInputChange}
/>
<div className="space-y-2">
<Label htmlFor="password">
Password <span className="text-destructive">*</span>
</Label>
<Input id="password" type="password" placeholder="" value={state.userCreate.password} onChange={handlePasswordInputChange} />
</div>
)}
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Role <span className="text-red-600">*</span>
</span>
<div className="w-full flex flex-row justify-start items-center text-base">
<RadioGroup orientation="horizontal" value={state.userCreate.role} onChange={handleRoleInputChange}>
<Radio value={Role.USER} label={"User"} />
<Radio value={Role.ADMIN} label={"Admin"} />
</RadioGroup>
</div>
</div>
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
<Button color="neutral" variant="plain" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onClose}>
{t("common.cancel")}
</Button>
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={handleSaveBtnClick}>
{t("common.save")}
</Button>
<div className="space-y-2">
<Label>
Role <span className="text-destructive">*</span>
</Label>
<RadioGroup value={state.userCreate.role} onValueChange={handleRoleChange}>
<div className="flex flex-row space-x-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value={Role.USER} id="role-user" />
<Label htmlFor="role-user" className="font-normal cursor-pointer">
User
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value={Role.ADMIN} id="role-admin" />
<Label htmlFor="role-admin" className="font-normal cursor-pointer">
Admin
</Label>
</div>
</div>
</RadioGroup>
</div>
</div>
</ModalDialog>
</Modal>
<DialogFooter>
<Button variant="outline" disabled={requestState.isLoading} onClick={onClose}>
{t("common.cancel")}
</Button>
<Button disabled={requestState.isLoading} onClick={handleSaveBtnClick}>
{requestState.isLoading ? "Saving..." : t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -1,27 +0,0 @@
import { useWorkspaceStore } from "@/stores";
import Icon from "./Icon";
const DemoBanner: React.FC = () => {
const workspaceStore = useWorkspaceStore();
const shouldShow = workspaceStore.profile.mode === "demo";
if (!shouldShow) return null;
return (
<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-8xl px-4 md:px-12 flex flex-row justify-between items-center gap-x-3">
<span>🔗 Slash - An open source, self-hosted platform for sharing and managing your most frequently used links.</span>
<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"
href="https://github.com/yourselfhosted/slash#deploy-with-docker-in-seconds"
target="_blank"
>
Install
<Icon.ExternalLink className="w-4 h-auto ml-1" />
</a>
</div>
</div>
);
};
export default DemoBanner;
@@ -1,10 +1,12 @@
import { Button, Input, Modal, ModalDialog } from "@mui/joy";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import useLoading from "@/hooks/useLoading";
import { useUserStore } from "@/stores";
import Icon from "./Icon";
interface Props {
onClose: () => void;
@@ -19,10 +21,6 @@ const EditUserinfoDialog: React.FC<Props> = (props: Props) => {
const [nickname, setNickname] = useState(currentUser.nickname);
const requestState = useLoading(false);
const handleCloseBtnClick = () => {
onClose();
};
const handleEmailChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value as string;
setEmail(text);
@@ -50,7 +48,7 @@ const EditUserinfoDialog: React.FC<Props> = (props: Props) => {
["email", "nickname"],
);
onClose();
toast("User information updated");
toast.success("User information updated");
} catch (error: any) {
console.error(error);
toast.error(error.details);
@@ -59,34 +57,31 @@ const EditUserinfoDialog: React.FC<Props> = (props: Props) => {
};
return (
<Modal open={true}>
<ModalDialog>
<div className="flex flex-row justify-between items-center w-80">
<span className="text-lg font-medium">Edit Userinfo</span>
<Button variant="plain" onClick={handleCloseBtnClick}>
<Icon.X className="w-5 h-auto text-gray-600" />
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="w-80 sm:max-w-md">
<DialogHeader>
<DialogTitle>Edit Userinfo</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">{t("common.email")}</Label>
<Input id="email" type="text" value={email} onChange={handleEmailChanged} />
</div>
<div className="space-y-2">
<Label htmlFor="nickname">{t("user.nickname")}</Label>
<Input id="nickname" type="text" value={nickname} onChange={handleNicknameChanged} />
</div>
</div>
<DialogFooter>
<Button variant="outline" disabled={requestState.isLoading} onClick={onClose}>
{t("common.cancel")}
</Button>
</div>
<div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">{t("common.email")}</span>
<Input className="w-full" type="text" value={email} onChange={handleEmailChanged} />
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">{t("user.nickname")}</span>
<Input className="w-full" type="text" value={nickname} onChange={handleNicknameChanged} />
</div>
<div className="w-full flex flex-row justify-end items-center space-x-2">
<Button variant="plain" disabled={requestState.isLoading} onClick={handleCloseBtnClick}>
{t("common.cancel")}
</Button>
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={handleSaveBtnClick}>
{t("common.save")}
</Button>
</div>
</div>
</ModalDialog>
</Modal>
<Button disabled={requestState.isLoading} onClick={handleSaveBtnClick}>
{requestState.isLoading ? "Saving..." : t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
+9 -4
View File
@@ -1,4 +1,4 @@
import { Tooltip } from "@mui/joy";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useWorkspaceStore } from "@/stores";
import { FeatureType } from "@/stores/workspace";
import Icon from "./Icon";
@@ -16,9 +16,14 @@ const FeatureBadge = ({ feature, className }: Props) => {
return null;
}
return (
<Tooltip title="This feature is not available on your plan." className={className} placement="top" arrow>
<Icon.Sparkles />
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild className={className}>
<Icon.Sparkles />
</TooltipTrigger>
<TooltipContent>This feature is not available on your plan.</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
+3 -3
View File
@@ -15,10 +15,10 @@ const FilterView = () => {
return (
<div className="w-full flex flex-row justify-start items-center mb-4 pl-2">
<span className="text-gray-400">Filters:</span>
<span className="text-muted-foreground">Filters:</span>
{filter.tag && (
<button
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-muted rounded-full text-muted-foreground text-sm hover:line-through"
onClick={() => viewStore.setFilter({ tag: undefined })}
>
<Icon.Tag className="w-4 h-auto mr-1" />
@@ -28,7 +28,7 @@ const FilterView = () => {
)}
{filter.visibility && (
<button
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-muted rounded-full text-muted-foreground text-sm hover:line-through"
onClick={() => viewStore.setFilter({ visibility: undefined })}
>
<VisibilityIcon className="w-4 h-auto mr-1" visibility={filter.visibility} />
@@ -1,10 +1,11 @@
import { Button, Modal, ModalDialog } from "@mui/joy";
import { QRCodeCanvas } from "qrcode.react";
import { useRef } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { absolutifyLink } from "@/helpers/utils";
import { Shortcut } from "@/types/proto/api/v1/shortcut_service";
import type { Shortcut } from "@/types/proto/api/v1/shortcut_service";
import Icon from "./Icon";
interface Props {
@@ -18,10 +19,6 @@ const GenerateQRCodeDialog: React.FC<Props> = (props: Props) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const shortcutLink = absolutifyLink(`/s/${shortcut.name}`);
const handleCloseBtnClick = () => {
onClose();
};
const handleDownloadQRCodeClick = () => {
const canvas = containerRef.current?.querySelector("canvas");
if (!canvas) {
@@ -33,31 +30,28 @@ const GenerateQRCodeDialog: React.FC<Props> = (props: Props) => {
link.download = `${shortcut.title || shortcut.name}-qrcode.png`;
link.href = canvas.toDataURL();
link.click();
handleCloseBtnClick();
onClose();
};
return (
<Modal open={true}>
<ModalDialog>
<div className="flex flex-row justify-between items-center w-64">
<span className="text-lg font-medium">QR Code</span>
<Button variant="plain" onClick={handleCloseBtnClick}>
<Icon.X className="w-5 h-auto text-gray-600" />
</Button>
</div>
<div>
<div ref={containerRef} className="w-full flex flex-row justify-center items-center mt-2 mb-6">
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="w-64 sm:max-w-xs">
<DialogHeader>
<DialogTitle>QR Code</DialogTitle>
</DialogHeader>
<div className="space-y-6">
<div ref={containerRef} className="w-full flex flex-row justify-center items-center">
<QRCodeCanvas value={shortcutLink} size={180} bgColor={"#ffffff"} fgColor={"#000000"} includeMargin={false} level={"L"} />
</div>
<div className="w-full flex flex-row justify-center items-center px-4">
<Button className="w-full" color="neutral" onClick={handleDownloadQRCodeClick}>
<div className="w-full flex flex-row justify-center items-center">
<Button className="w-full" variant="secondary" onClick={handleDownloadQRCodeClick}>
<Icon.Download className="w-4 h-auto mr-1" />
{t("common.download")}
</Button>
</div>
</div>
</ModalDialog>
</Modal>
</DialogContent>
</Dialog>
);
};
+16 -16
View File
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { Link, useLocation } from "react-router-dom";
import { authServiceClient } from "@/grpcweb";
import { useWorkspaceStore, useUserStore } from "@/stores";
import { stringifyPlanType } from "@/stores/subscription";
import { PlanType } from "@/types/proto/api/v1/subscription_service";
import { Role } from "@/types/proto/api/v1/user_service";
import AboutDialog from "./AboutDialog";
@@ -28,41 +29,40 @@ const Header: React.FC = () => {
return (
<>
<div className="w-full bg-gray-50 dark:bg-black border-b border-b-gray-200 dark:border-b-zinc-800">
<div className="w-full bg-muted/50 border-b border-border">
<div className="w-full max-w-8xl mx-auto px-4 sm:px-6 md:px-12 py-3 flex flex-row justify-between items-center">
<div className="flex flex-row justify-start items-center shrink mr-2">
<Link to="/" className="cursor-pointer flex flex-row justify-start items-center dark:text-gray-400" viewTransition>
<Link to="/" className="cursor-pointer flex flex-row justify-start items-center text-foreground" viewTransition>
<Logo className="mr-2" />
Slash
</Link>
{[PlanType.PRO, PlanType.ENTERPRISE].includes(subscription.plan) && (
<span className="ml-1 text-xs px-1.5 leading-5 border rounded-full bg-blue-600 border-blue-700 text-white shadow dark:opacity-70">
{/* PRO or ENT */}
{subscription.plan.substring(0, 3)}
<span className="ml-1 text-xs px-1.5 leading-5 border rounded-full bg-primary border-primary text-primary-foreground shadow">
{stringifyPlanType(subscription.plan)}
</span>
)}
{shouldShowRouterSwitch && (
<>
<span className="font-mono opacity-60 mx-1 dark:text-gray-400">/</span>
<span className="font-mono text-muted-foreground mx-1">/</span>
<Dropdown
trigger={
<button className="flex flex-row justify-end items-center cursor-pointer">
<span className="dark:text-gray-400">{selectedSection}</span>
<Icon.ChevronsUpDown className="ml-1 w-4 h-auto text-gray-600 dark:text-gray-400" />
<span className="text-foreground">{selectedSection}</span>
<Icon.ChevronsUpDown className="ml-1 w-4 h-auto text-muted-foreground" />
</button>
}
actionsClassName="!w-36 -left-4"
actions={
<>
<Link
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
className="w-full px-2 flex flex-row justify-start items-center text-left text-foreground leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
to="/shortcuts"
viewTransition
>
<Icon.SquareSlash className="w-5 h-auto mr-2 opacity-70" /> Shortcuts
</Link>
<Link
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
className="w-full px-2 flex flex-row justify-start items-center text-left text-foreground leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
to="/collections"
viewTransition
>
@@ -78,15 +78,15 @@ const Header: React.FC = () => {
<Dropdown
trigger={
<button className="flex flex-row justify-end items-center cursor-pointer">
<span className="dark:text-gray-400 max-w-20 truncate">{currentUser.nickname}</span>
<Icon.ChevronDown className="ml-1 w-5 h-auto text-gray-600 dark:text-gray-400" />
<span className="text-foreground max-w-20 truncate">{currentUser.nickname}</span>
<Icon.ChevronDown className="ml-1 w-5 h-auto text-muted-foreground" />
</button>
}
actionsClassName="!w-32"
actions={
<>
<Link
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
className="w-full px-2 flex flex-row justify-start items-center text-left text-foreground leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
to="/setting/general"
viewTransition
>
@@ -94,7 +94,7 @@ const Header: React.FC = () => {
</Link>
{isAdmin && (
<Link
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
className="w-full px-2 flex flex-row justify-start items-center text-left text-foreground leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
to="/setting/workspace"
viewTransition
>
@@ -102,13 +102,13 @@ const Header: React.FC = () => {
</Link>
)}
<button
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
className="w-full px-2 flex flex-row justify-start items-center text-left text-foreground leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
onClick={() => setShowAboutDialog(true)}
>
<Icon.Info className="w-5 h-auto mr-2 opacity-70" /> {t("common.about")}
</button>
<button
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
className="w-full px-2 flex flex-row justify-start items-center text-left text-foreground leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
onClick={() => handleSignOutButtonClick()}
>
<Icon.LogOut className="w-5 h-auto mr-2 opacity-70" /> {t("auth.sign-out")}
+1 -1
View File
@@ -28,7 +28,7 @@ const LinkFavicon = (props: Props) => {
return faviconUrl ? (
<img className="w-full h-auto rounded" src={faviconUrl} decoding="async" loading="lazy" onError={handleImgError} />
) : (
<Icon.CircleSlash className="w-full h-auto text-gray-400" strokeWidth={1.5} />
<Icon.CircleSlash className="w-full h-auto text-muted-foreground" strokeWidth={1.5} />
);
};
+1 -1
View File
@@ -12,7 +12,7 @@ const Logo = ({ className }: Props) => {
const hasCustomBranding = workspaceStore.checkFeatureAvailable(FeatureType.CustomeBranding);
const branding = hasCustomBranding && workspaceStore.setting.branding ? new TextDecoder().decode(workspaceStore.setting.branding) : "";
return (
<div className={classNames("w-8 h-auto dark:text-gray-500 rounded-lg overflow-hidden", className)}>
<div className={classNames("w-8 h-auto text-muted-foreground rounded-lg overflow-hidden", className)}>
{branding ? (
<img src={branding} alt="branding" className="max-w-full max-h-full" />
) : (
@@ -1,7 +1,8 @@
import { Button, Input } from "@mui/joy";
import { FormEvent, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { authServiceClient } from "@/grpcweb";
import useLoading from "@/hooks/useLoading";
import useNavigateTo from "@/hooks/useNavigateTo";
@@ -53,7 +54,7 @@ const PasswordAuthForm = () => {
<form className="w-full mt-6" onSubmit={handleSigninBtnClick}>
<div className={`flex flex-col justify-start items-start w-full ${actionBtnLoadingState.isLoading ? "opacity-80" : ""}`}>
<div className="w-full flex flex-col mb-2">
<span className="leading-8 mb-1 text-gray-600">{t("common.email")}</span>
<span className="leading-8 mb-1 text-muted-foreground">{t("common.email")}</span>
<Input
className="w-full py-3"
type="email"
@@ -63,20 +64,13 @@ const PasswordAuthForm = () => {
/>
</div>
<div className="w-full flex flex-col mb-2">
<span className="leading-8 text-gray-600">{t("common.password")}</span>
<span className="leading-8 text-muted-foreground">{t("common.password")}</span>
<Input className="w-full py-3" type="password" value={password} placeholder="····" onChange={handlePasswordInputChanged} />
</div>
</div>
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
<Button
className="w-full"
type="submit"
color="primary"
loading={actionBtnLoadingState.isLoading}
disabled={actionBtnLoadingState.isLoading || !allowConfirm}
onClick={handleSigninBtnClick}
>
{t("auth.sign-in")}
<Button className="w-full" type="submit" disabled={actionBtnLoadingState.isLoading || !allowConfirm} onClick={handleSigninBtnClick}>
{actionBtnLoadingState.isLoading ? "Loading..." : t("auth.sign-in")}
</Button>
</div>
</form>
@@ -1,6 +1,7 @@
import { IconButton, Input } from "@mui/joy";
import classNames from "classnames";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { generateRandomString } from "@/helpers/utils";
import Icon from "./Icon";
@@ -37,12 +38,12 @@ const ResourceNameInput = (props: Props) => {
<span>:</span>
<span className="ml-1 font-mono font-medium">{editingName}</span>
<div className="ml-1 flex flex-row justify-start items-center">
<IconButton size="sm" variant="plain" color="neutral" onClick={() => setModified(true)}>
<Icon.Edit className="w-4 h-auto text-gray-500 dark:text-gray-400" />
</IconButton>
<IconButton size="sm" variant="plain" color="neutral" onClick={() => setEditingName(generateRandomString().toLowerCase())}>
<Icon.RefreshCcw className="w-4 h-auto text-gray-500 dark:text-gray-400" />
</IconButton>
<Button size="icon" variant="ghost" onClick={() => setModified(true)}>
<Icon.Edit className="w-4 h-auto text-muted-foreground" />
</Button>
<Button size="icon" variant="ghost" onClick={() => setEditingName(generateRandomString().toLowerCase())}>
<Icon.RefreshCcw className="w-4 h-auto text-muted-foreground" />
</Button>
</div>
</>
)}
@@ -42,32 +42,32 @@ const ShortcutActionsDropdown = (props: Props) => {
return (
<>
<Dropdown
actionsClassName="!w-32 dark:text-gray-500 text-sm"
actionsClassName="!w-32 text-sm"
actions={
<>
{havePermission && (
<button
className="w-full px-2 flex flex-row justify-start items-center text-left leading-8 cursor-pointer rounded hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60 dark:hover:bg-zinc-800"
className="w-full px-2 flex flex-row justify-start items-center text-left text-foreground leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
onClick={() => setShowEditDrawer(true)}
>
<Icon.Edit className="w-4 h-auto mr-2 opacity-70" /> {t("common.edit")}
</button>
)}
<button
className="w-full px-2 flex flex-row justify-start items-center text-left leading-8 cursor-pointer rounded hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60 dark:hover:bg-zinc-800"
className="w-full px-2 flex flex-row justify-start items-center text-left text-foreground leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
onClick={() => setShowQRCodeDialog(true)}
>
<Icon.QrCode className="w-4 h-auto mr-2 opacity-70" /> QR Code
</button>
<button
className="w-full px-2 flex flex-row justify-start items-center text-left leading-8 cursor-pointer rounded hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60 dark:hover:bg-zinc-800"
className="w-full px-2 flex flex-row justify-start items-center text-left text-foreground leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
onClick={gotoAnalytics}
>
<Icon.BarChart2 className="w-4 h-auto mr-2 opacity-70" /> {t("analytics.self")}
</button>
{havePermission && (
<button
className="w-full px-2 flex flex-row justify-start items-center text-left leading-8 cursor-pointer rounded text-red-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60 dark:hover:bg-zinc-800"
className="w-full px-2 flex flex-row justify-start items-center text-left leading-8 cursor-pointer rounded text-destructive hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
onClick={() => {
handleDeleteShortcutButtonClick(shortcut);
}}
+71 -55
View File
@@ -1,10 +1,13 @@
import { Avatar, Tooltip } from "@mui/joy";
import classNames from "classnames";
import copy from "copy-to-clipboard";
import { useEffect } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { absolutifyLink } from "@/helpers/utils";
import { useUserStore, useViewStore } from "@/stores";
import { Shortcut } from "@/types/proto/api/v1/shortcut_service";
@@ -35,52 +38,53 @@ const ShortcutCard = (props: Props) => {
};
return (
<div
className={classNames(
"group px-4 py-3 w-full flex flex-col justify-start items-start border rounded-lg hover:shadow dark:border-zinc-700",
)}
>
<Card className={classNames("group p-4 w-full flex flex-col justify-start items-start hover:shadow-md transition-shadow duration-200")}>
<div className="w-full flex flex-row justify-between items-center">
<div className="w-[calc(100%-16px)] flex flex-row justify-start items-center mr-1 shrink-0">
<Link
className={classNames("w-8 h-8 flex justify-center items-center overflow-clip shrink-0")}
className={classNames("w-8 h-8 flex justify-center items-center overflow-clip shrink-0 rounded")}
to={`/shortcut/${shortcut.id}`}
viewTransition
>
<LinkFavicon url={shortcut.link} />
</Link>
<div className="ml-2 w-[calc(100%-24px)] flex flex-col justify-start items-start">
<div className="ml-3 w-[calc(100%-24px)] flex flex-col justify-start items-start">
<div className="w-full flex flex-row justify-start items-center leading-tight">
<a
className={classNames(
"max-w-[calc(100%-36px)] flex flex-row justify-start items-center mr-1 cursor-pointer hover:opacity-80 hover:underline",
"max-w-[calc(100%-36px)] flex flex-row justify-start items-center mr-1 hover:opacity-80 hover:underline transition-all",
)}
target="_blank"
href={shortcutLink}
>
<div className="truncate">
<span className="dark:text-gray-400">{shortcut.title}</span>
<span className="text-foreground font-medium">{shortcut.title}</span>
{shortcut.title ? (
<span className="text-gray-500">({shortcut.name})</span>
<span className="text-muted-foreground ml-1">({shortcut.name})</span>
) : (
<span className="truncate dark:text-gray-400">{shortcut.name}</span>
<span className="truncate text-foreground font-medium">{shortcut.name}</span>
)}
</div>
<span className="hidden group-hover:block ml-1 cursor-pointer shrink-0">
<Icon.ExternalLink className="w-4 h-auto text-gray-600" />
<span className="hidden group-hover:block ml-1 shrink-0">
<Icon.ExternalLink className="w-4 h-auto text-muted-foreground" />
</span>
</a>
<Tooltip title="Copy" variant="solid" placement="top" arrow>
<button
className="hidden group-hover:block cursor-pointer text-gray-500 hover:opacity-80"
onClick={() => handleCopyButtonClick()}
>
<Icon.Clipboard className="w-4 h-auto mx-auto" />
</button>
<Tooltip>
<TooltipTrigger asChild>
<button
className="hidden group-hover:block text-muted-foreground hover:text-foreground transition-colors"
onClick={() => handleCopyButtonClick()}
>
<Icon.Clipboard className="w-4 h-auto" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Copy</p>
</TooltipContent>
</Tooltip>
</div>
<a
className="pr-4 leading-tight w-full text-sm truncate text-gray-400 dark:text-gray-500 hover:underline"
className="pr-4 leading-tight w-full text-sm truncate text-muted-foreground hover:underline transition-all"
href={shortcut.link}
target="_blank"
>
@@ -92,51 +96,63 @@ const ShortcutCard = (props: Props) => {
<ShortcutActionsDropdown shortcut={shortcut} />
</div>
</div>
<div className="mt-2 w-full flex flex-row justify-start items-start gap-2 truncate">
<div className="mt-3 w-full flex flex-row justify-start items-start gap-2 truncate">
{shortcut.tags.map((tag) => {
return (
<span
<Badge
key={tag}
className="max-w-[8rem] truncate text-gray-400 dark:text-gray-500 text-sm leading-4 cursor-pointer hover:opacity-80"
variant="secondary"
className="max-w-[8rem] truncate cursor-pointer hover:bg-secondary/80 transition-colors"
onClick={() => viewStore.setFilter({ tag: tag })}
>
#{tag}
</span>
</Badge>
);
})}
{shortcut.tags.length === 0 && <span className="text-gray-400 text-sm leading-4 italic">No tags</span>}
{shortcut.tags.length === 0 && <span className="text-muted-foreground text-sm italic">No tags</span>}
</div>
<div className="w-full mt-2 flex gap-2 overflow-x-auto">
<Tooltip title={creator.nickname} variant="solid" placement="top" arrow>
<Avatar
className="dark:bg-zinc-800"
sx={{
"--Avatar-size": "24px",
}}
alt={creator.nickname.toUpperCase()}
></Avatar>
<div className="w-full mt-3 flex gap-3 overflow-x-auto">
<Tooltip>
<TooltipTrigger asChild>
<Avatar className="h-6 w-6">
<AvatarFallback className="text-xs">{creator.nickname.substring(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
</TooltipTrigger>
<TooltipContent>
<p>{creator.nickname}</p>
</TooltipContent>
</Tooltip>
<Tooltip title={t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.description`)} variant="solid" placement="top" arrow>
<div
className="w-auto leading-5 flex flex-row justify-start items-center flex-nowrap whitespace-nowrap cursor-pointer text-gray-400 text-sm"
onClick={() => viewStore.setFilter({ visibility: shortcut.visibility })}
>
<VisibilityIcon className="w-4 h-auto mr-1 opacity-70" visibility={shortcut.visibility} />
{t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.self`)}
</div>
<Tooltip>
<TooltipTrigger asChild>
<div
className="flex flex-row justify-start items-center gap-1 text-muted-foreground text-sm cursor-pointer hover:text-foreground transition-colors"
onClick={() => viewStore.setFilter({ visibility: shortcut.visibility })}
>
<VisibilityIcon className="w-4 h-auto" visibility={shortcut.visibility} />
{t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.self`)}
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.description`)}</p>
</TooltipContent>
</Tooltip>
<Tooltip title="View count" variant="solid" placement="top" arrow>
<Link
className="w-auto leading-5 flex flex-row justify-start items-center flex-nowrap whitespace-nowrap cursor-pointer text-gray-400 text-sm"
to={`/shortcut/${shortcut.id}#analytics`}
viewTransition
>
<Icon.BarChart2 className="w-4 h-auto mr-1 opacity-70" />
{t("shortcut.visits", { count: shortcut.viewCount })}
</Link>
<Tooltip>
<TooltipTrigger asChild>
<Link
className="flex flex-row justify-start items-center gap-1 text-muted-foreground text-sm hover:text-foreground transition-colors"
to={`/shortcut/${shortcut.id}#analytics`}
viewTransition
>
<Icon.BarChart2 className="w-4 h-auto" />
{t("shortcut.visits", { count: shortcut.viewCount })}
</Link>
</TooltipTrigger>
<TooltipContent>
<p>View count</p>
</TooltipContent>
</Tooltip>
</div>
</div>
</Card>
);
};
@@ -1,6 +1,6 @@
import { Divider } from "@mui/joy";
import classNames from "classnames";
import { Link } from "react-router-dom";
import { Separator } from "@/components/ui/separator";
import { Shortcut } from "@/types/proto/api/v1/shortcut_service";
import Icon from "./Icon";
import LinkFavicon from "./LinkFavicon";
@@ -13,7 +13,7 @@ const ShortcutFrame = ({ shortcut }: Props) => {
return (
<div className="w-full h-full flex flex-col justify-center items-center p-8">
<Link
className="w-72 max-w-full border dark:border-zinc-900 dark:bg-zinc-900 p-6 pb-4 rounded-2xl shadow-xl dark:text-gray-400 hover:opacity-80"
className="w-72 max-w-full border border-border bg-card text-card-foreground p-6 pb-4 rounded-2xl shadow-xl hover:opacity-80"
to={`/s/${shortcut.name}`}
target="_blank"
>
@@ -21,9 +21,9 @@ const ShortcutFrame = ({ shortcut }: Props) => {
<LinkFavicon url={shortcut.link} />
</div>
<p className="text-lg font-medium leading-8 mt-2 truncate">{shortcut.title || shortcut.name}</p>
<p className="text-gray-500 truncate">{shortcut.description}</p>
<Divider className="!my-2" />
<p className="text-gray-400 dark:text-gray-600 text-sm mt-2">
<p className="text-muted-foreground truncate">{shortcut.description}</p>
<Separator className="my-2" />
<p className="text-muted-foreground text-sm mt-2">
<span className="leading-4">Open this site in a new tab</span>
<Icon.ArrowUpRight className="inline-block ml-1 -mt-0.5 w-4 h-auto" />
</p>
+6 -6
View File
@@ -19,7 +19,7 @@ const ShortcutView = (props: Props) => {
return (
<div
className={classNames(
"group w-full px-3 py-2 flex flex-row justify-start items-center border rounded-lg hover:bg-gray-100 dark:border-zinc-800 dark:hover:bg-zinc-800",
"group w-full px-3 py-2 flex flex-row justify-start items-center border border-border rounded-lg hover:bg-accent",
className,
)}
onClick={onClick}
@@ -30,25 +30,25 @@ const ShortcutView = (props: Props) => {
<div className="ml-2 w-full truncate">
{shortcut.title ? (
<>
<span className="dark:text-gray-400">{shortcut.title}</span>
<span className="text-gray-500">({shortcut.name})</span>
<span className="text-foreground">{shortcut.title}</span>
<span className="text-muted-foreground">({shortcut.name})</span>
</>
) : (
<>
<span className="dark:text-gray-400">{shortcut.name}</span>
<span className="text-foreground">{shortcut.name}</span>
</>
)}
</div>
<Link
className={classNames(
"hidden group-hover:block ml-1 w-6 h-6 p-1 shrink-0 rounded-lg bg-gray-200 dark:bg-zinc-900 hover:opacity-80",
"hidden group-hover:block ml-1 w-6 h-6 p-1 shrink-0 rounded-lg bg-muted hover:opacity-80",
alwaysShowLink && "!block",
)}
to={`/s/${shortcut.name}`}
target="_blank"
onClick={(e) => e.stopPropagation()}
>
<Icon.ArrowUpRight className="w-4 h-auto text-gray-400 shrink-0" />
<Icon.ArrowUpRight className="w-4 h-auto text-muted-foreground shrink-0" />
</Link>
{showActions && (
<div className="ml-1 flex flex-row justify-end items-center shrink-0" onClick={(e) => e.stopPropagation()}>
@@ -15,10 +15,8 @@ const ShortcutsNavigator = () => {
<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 dark:text-gray-400 rounded-md",
currentTab === "tab:all"
? "bg-blue-700 dark:bg-blue-800 text-white dark:text-gray-400 shadow"
: "hover:bg-gray-200 dark:hover:bg-zinc-700",
"flex flex-row justify-center items-center px-2 leading-7 text-sm rounded-md",
currentTab === "tab:all" ? "bg-primary text-primary-foreground shadow" : "text-foreground hover:bg-accent",
)}
onClick={() => viewStore.setFilter({ tab: "tab:all" })}
>
@@ -27,10 +25,8 @@ const ShortcutsNavigator = () => {
</button>
<button
className={classNames(
"flex flex-row justify-center items-center px-2 leading-7 text-sm dark:text-gray-400 rounded-md",
currentTab === "tab:mine"
? "bg-blue-700 dark:bg-blue-800 text-white dark:text-gray-400 shadow"
: "hover:bg-gray-200 dark:hover:bg-zinc-700",
"flex flex-row justify-center items-center px-2 leading-7 text-sm rounded-md",
currentTab === "tab:mine" ? "bg-primary text-primary-foreground shadow" : "text-foreground hover:bg-accent",
)}
onClick={() => viewStore.setFilter({ tab: "tab:mine" })}
>
@@ -41,10 +37,8 @@ const ShortcutsNavigator = () => {
<button
key={tag}
className={classNames(
"flex flex-row justify-center items-center px-2 leading-7 text-sm dark:text-gray-400 rounded-md",
currentTab === `tag:${tag}`
? "bg-blue-700 dark:bg-blue-800 text-white dark:text-gray-400 shadow"
: "hover:bg-gray-200 dark:hover:bg-zinc-700",
"flex flex-row justify-center items-center px-2 leading-7 text-sm rounded-md",
currentTab === `tag:${tag}` ? "bg-primary text-primary-foreground shadow" : "text-foreground hover:bg-accent",
)}
onClick={() => viewStore.setFilter({ tab: `tag:${tag}`, tag: undefined })}
>
+23 -26
View File
@@ -1,38 +1,35 @@
import Accordion from "@mui/joy/Accordion";
import AccordionDetails from "@mui/joy/AccordionDetails";
import AccordionGroup from "@mui/joy/AccordionGroup";
import AccordionSummary from "@mui/joy/AccordionSummary";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
const SubscriptionFAQ = () => {
return (
<div className="w-full flex flex-col justify-center items-center">
<h2 className="text-2xl font-semibold mb-8 dark:text-gray-400">Frequently Asked Questions</h2>
<AccordionGroup className="w-full max-w-2xl">
<Accordion>
<AccordionSummary>Can I use the Free plan in my team?</AccordionSummary>
<AccordionDetails>
<h2 className="text-2xl font-semibold mb-8 text-foreground">Frequently Asked Questions</h2>
<Accordion type="single" collapsible className="w-full max-w-2xl">
<AccordionItem value="item-1">
<AccordionTrigger>Can I use the Free plan in my team?</AccordionTrigger>
<AccordionContent>
Of course you can. In the free plan, you can invite up to 5 members to your team. If you need more, you should upgrade to the
Pro plan.
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary>How many devices can the license key be used on?</AccordionSummary>
<AccordionDetails>{`It's unlimited for now, but please do not abuse it.`}</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary>{`Can I get a refund if Slash doesn't meet my needs?`}</AccordionSummary>
<AccordionDetails>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>How many devices can the license key be used on?</AccordionTrigger>
<AccordionContent>{`It's unlimited for now, but please do not abuse it.`}</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>{`Can I get a refund if Slash doesn't meet my needs?`}</AccordionTrigger>
<AccordionContent>
Yes, absolutely! You can contact us with `yourselfhosted@gmail.com`. I will refund you as soon as possible.
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary>Is there a Lifetime license?</AccordionSummary>
<AccordionDetails>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-4">
<AccordionTrigger>Is there a Lifetime license?</AccordionTrigger>
<AccordionContent>
{`As software requires someone to maintain it, so we won't sell a lifetime service, since humans are not immortal yet. But if you
really want it, please contact us "yourselfhosted@gmail.com".`}
</AccordionDetails>
</Accordion>
</AccordionGroup>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};
+24 -13
View File
@@ -1,5 +1,7 @@
import { Divider, Option, Select, Switch } from "@mui/joy";
import { useTranslation } from "react-i18next";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { useViewStore } from "@/stores";
import Icon from "./Icon";
import Dropdown from "./common/Dropdown";
@@ -15,7 +17,7 @@ const ViewSetting = () => {
<Dropdown
trigger={
<button>
<Icon.Settings2 className="w-4 h-auto text-gray-500" />
<Icon.Settings2 className="w-4 h-auto text-muted-foreground" />
</button>
}
actionsClassName="!mt-3 !right-[unset] -left-24 -ml-2"
@@ -24,26 +26,35 @@ const ViewSetting = () => {
<div className="w-full flex flex-row justify-between items-center">
<span className="text-sm shrink-0 mr-2">{t("filter.compact-mode")}</span>
<Switch
size="sm"
checked={displayStyle === "compact"}
onChange={(event) => viewStore.setDisplayStyle(event.target.checked ? "compact" : "full")}
onCheckedChange={(checked) => viewStore.setDisplayStyle(checked ? "compact" : "full")}
/>
</div>
<Divider className="!my-1" />
<Separator className="!my-1" />
<div className="w-full flex flex-row justify-between items-center">
<span className="text-sm shrink-0 mr-2">{t("filter.order-by")}</span>
<Select size="sm" value={field} onChange={(_, value) => viewStore.setOrder({ field: value as any })}>
<Option value={"name"}>Name</Option>
<Option value={"updatedTs"}>CreatedAt</Option>
<Option value={"createdTs"}>UpdatedAt</Option>
<Option value={"view"}>Visits</Option>
<Select value={field} onValueChange={(value) => viewStore.setOrder({ field: value as any })}>
<SelectTrigger className="w-32 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="name">Name</SelectItem>
<SelectItem value="updatedTs">CreatedAt</SelectItem>
<SelectItem value="createdTs">UpdatedAt</SelectItem>
<SelectItem value="view">Visits</SelectItem>
</SelectContent>
</Select>
</div>
<div className="w-full flex flex-row justify-between items-center">
<span className="text-sm shrink-0 mr-2">{t("filter.direction")}</span>
<Select size="sm" value={direction} onChange={(_, value) => viewStore.setOrder({ direction: value as any })}>
<Option value={"asc"}>ASC</Option>
<Option value={"desc"}>DESC</Option>
<Select value={direction} onValueChange={(value) => viewStore.setOrder({ direction: value as any })}>
<SelectTrigger className="w-32 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="asc">ASC</SelectItem>
<SelectItem value="desc">DESC</SelectItem>
</SelectContent>
</Select>
</div>
</div>
+16 -45
View File
@@ -1,5 +1,6 @@
import { ReactNode, useEffect, useRef, useState } from "react";
import { ReactNode, useState } from "react";
import Icon from "@/components/Icon";
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
interface Props {
trigger?: ReactNode;
@@ -10,54 +11,24 @@ interface Props {
const Dropdown: React.FC<Props> = (props: Props) => {
const { trigger, actions, className, actionsClassName } = props;
const [dropdownStatus, setDropdownStatus] = useState(false);
const dropdownWrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (dropdownStatus) {
const handleClickOutside = (event: MouseEvent) => {
if (!dropdownWrapperRef.current?.contains(event.target as Node)) {
setDropdownStatus(false);
}
};
window.addEventListener("click", handleClickOutside, {
capture: true,
});
return () => {
window.removeEventListener("click", handleClickOutside, {
capture: true,
});
};
}
}, [dropdownStatus]);
const handleToggleDropdownStatus = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
setDropdownStatus(!dropdownStatus);
};
const [open, setOpen] = useState(false);
return (
<div
ref={dropdownWrapperRef}
className={`relative flex flex-col justify-start items-start select-none ${className ?? ""}`}
onClick={handleToggleDropdownStatus}
>
{trigger ? (
trigger
) : (
<button className="flex flex-row justify-center items-center rounded text-gray-400 cursor-pointer hover:text-gray-500">
<Icon.MoreVertical className="w-4 h-auto" />
</button>
)}
<div
className={`w-auto mt-1 absolute top-full right-0 flex flex-col justify-start items-start bg-white dark:bg-zinc-900 z-1 border dark:border-zinc-800 p-1 rounded-md shadow ${
actionsClassName ?? ""
} ${dropdownStatus ? "" : "!hidden"}`}
>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger className={className} asChild>
{trigger ? (
<div>{trigger}</div>
) : (
<button className="flex flex-row justify-center items-center rounded text-muted-foreground hover:text-foreground transition-colors">
<Icon.MoreVertical className="w-4 h-auto" />
</button>
)}
</DropdownMenuTrigger>
<DropdownMenuContent className={actionsClassName} align="end" onClick={() => setOpen(false)}>
{actions}
</div>
</div>
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -1,11 +1,11 @@
import { Button, IconButton } from "@mui/joy";
import copy from "copy-to-clipboard";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { showCommonDialog } from "@/components/Alert";
import CreateAccessTokenDialog from "@/components/CreateAccessTokenDialog";
import Icon from "@/components/Icon";
import { Button } from "@/components/ui/button";
import { userServiceClient } from "@/grpcweb";
import { useUserStore } from "@/stores";
import { UserAccessToken } from "@/types/proto/api/v1/user_service";
@@ -64,13 +64,12 @@ const AccessTokenSection = () => {
<div className="w-full">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<p className="text-2xl shrink-0 font-semibold text-gray-900 dark:text-gray-500">Access Tokens</p>
<p className="mt-2 text-sm text-gray-700 dark:text-gray-600">A list of all access tokens for your account.</p>
<p className="text-2xl shrink-0 font-semibold text-foreground">Access Tokens</p>
<p className="mt-2 text-sm text-muted-foreground">A list of all access tokens for your account.</p>
</div>
<div className="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<Button
variant="outlined"
color="neutral"
variant="outline"
onClick={() => {
setShowCreateDialog(true);
}}
@@ -82,19 +81,19 @@ const AccessTokenSection = () => {
<div className="mt-2 flow-root">
<div className="overflow-x-auto">
<div className="inline-block min-w-full py-2 align-middle">
<table className="min-w-full divide-y divide-gray-300 dark:divide-zinc-700">
<table className="min-w-full divide-y divide-border">
<thead>
<tr>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-foreground">
Token
</th>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-foreground">
Description
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-foreground">
Created At
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-foreground">
Expires At
</th>
<th scope="col" className="relative py-3.5 pl-3 pr-4">
@@ -102,33 +101,32 @@ const AccessTokenSection = () => {
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-zinc-800">
<tbody className="divide-y divide-border">
{userAccessTokens.map((userAccessToken) => (
<tr key={userAccessToken.accessToken}>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-900 flex flex-row justify-start items-center gap-x-1 dark:text-gray-500">
<td className="whitespace-nowrap px-3 py-4 text-sm text-foreground flex flex-row justify-start items-center gap-x-1">
<span className="font-mono">{getFormatedAccessToken(userAccessToken.accessToken)}</span>
<Button color="neutral" variant="plain" size="sm" onClick={() => copyAccessToken(userAccessToken.accessToken)}>
<Icon.Clipboard className="w-4 h-auto text-gray-500" />
<Button variant="ghost" size="sm" onClick={() => copyAccessToken(userAccessToken.accessToken)}>
<Icon.Clipboard className="w-4 h-auto text-muted-foreground" />
</Button>
</td>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm text-gray-900 dark:text-gray-500">
{userAccessToken.description}
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm text-foreground">{userAccessToken.description}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-muted-foreground">
{userAccessToken.issuedAt?.toLocaleString()}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{userAccessToken.issuedAt?.toLocaleString()}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<td className="whitespace-nowrap px-3 py-4 text-sm text-muted-foreground">
{userAccessToken.expiresAt?.toLocaleString() ?? "Never"}
</td>
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm">
<IconButton
color="danger"
variant="plain"
<Button
variant="ghost"
size="sm"
onClick={() => {
handleDeleteAccessToken(userAccessToken.accessToken);
}}
>
<Icon.Trash className="w-4 h-auto" />
</IconButton>
</Button>
</td>
</tr>
))}
@@ -1,8 +1,8 @@
import { Button } from "@mui/joy";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import ChangePasswordDialog from "@/components/ChangePasswordDialog";
import EditUserinfoDialog from "@/components/EditUserinfoDialog";
import { Button } from "@/components/ui/button";
import { useUserStore } from "@/stores";
import { Role } from "@/types/proto/api/v1/user_service";
@@ -16,20 +16,20 @@ const AccountSection: React.FC = () => {
return (
<>
<div className="w-full flex flex-col justify-start items-start gap-y-2">
<p className="text-2xl shrink-0 font-semibold text-gray-900 dark:text-gray-500">{t("common.account")}</p>
<p className="flex flex-row justify-start items-center mt-2 dark:text-gray-400">
<p className="text-2xl shrink-0 font-semibold text-foreground">{t("common.account")}</p>
<p className="flex flex-row justify-start items-center mt-2 text-foreground">
<span className="text-xl">{currentUser.nickname}</span>
{isAdmin && <span className="ml-2 bg-blue-600 text-white px-2 leading-6 text-sm rounded-full">Admin</span>}
</p>
<p className="flex flex-row justify-start items-center dark:text-gray-400">
<span className="mr-3 text-gray-500">{t("common.email")}: </span>
<p className="flex flex-row justify-start items-center text-foreground">
<span className="mr-3 text-muted-foreground">{t("common.email")}: </span>
{currentUser.email}
</p>
<div className="flex flex-row justify-start items-center gap-2 mt-2">
<Button variant="outlined" color="neutral" onClick={() => setShowEditUserinfoDialog(true)}>
<Button variant="outline" onClick={() => setShowEditUserinfoDialog(true)}>
{t("common.edit")}
</Button>
<Button variant="outlined" color="neutral" onClick={() => setShowChangePasswordDialog(true)}>
<Button variant="outline" onClick={() => setShowChangePasswordDialog(true)}>
Change password
</Button>
</div>
@@ -1,6 +1,6 @@
import { Option, Select } from "@mui/joy";
import { useTranslation } from "react-i18next";
import BetaBadge from "@/components/BetaBadge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useUserStore } from "@/stores";
import { UserSetting } from "@/types/proto/api/v1/user_setting_service";
@@ -85,35 +85,45 @@ const PreferenceSection: React.FC = () => {
return (
<div className="w-full flex flex-col sm:flex-row justify-start items-start gap-4 sm:gap-x-16">
<p className="sm:w-1/4 text-2xl shrink-0 font-semibold text-gray-900 dark:text-gray-500">{t("settings.preference.self")}</p>
<p className="sm:w-1/4 text-2xl shrink-0 font-semibold text-foreground">{t("settings.preference.self")}</p>
<div className="w-full sm:w-auto grow flex flex-col justify-start items-start gap-4">
<div className="w-full flex flex-row justify-between items-center">
<div className="flex flex-row justify-start items-center gap-x-1">
<span className="dark:text-gray-400">{t("settings.preference.color-theme")}</span>
<span className="text-foreground">{t("settings.preference.color-theme")}</span>
</div>
<Select defaultValue={colorTheme} onChange={(_, value) => handleSelectColorTheme(value as string)}>
{colorThemeOptions.map((option) => {
return (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
);
})}
<Select defaultValue={colorTheme} onValueChange={handleSelectColorTheme}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{colorThemeOptions.map((option) => {
return (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<div className="w-full flex flex-row justify-between items-center">
<div className="flex flex-row justify-start items-center gap-x-1">
<span className="dark:text-gray-400">{t("common.language")}</span>
<span className="text-foreground">{t("common.language")}</span>
<BetaBadge />
</div>
<Select defaultValue={language} onChange={(_, value) => handleSelectLanguage(value as string)}>
{languageOptions.map((option) => {
return (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
);
})}
<Select defaultValue={language} onValueChange={handleSelectLanguage}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{languageOptions.map((option) => {
return (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
</div>
@@ -1,7 +1,7 @@
import { Button, IconButton } from "@mui/joy";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { workspaceServiceClient } from "@/grpcweb";
import { useWorkspaceStore } from "@/stores";
import { FeatureType } from "@/stores/workspace";
@@ -60,7 +60,7 @@ const SSOSection = () => {
<div className="w-full flex flex-col gap-2 pt-2 pb-4">
<div className="w-full flex flex-row justify-between items-center gap-1">
<div className="flex flex-row justify-start items-center">
<span className="font-medium dark:text-gray-400">SSO</span>
<span className="font-medium text-foreground">SSO</span>
<FeatureBadge className="w-5 h-auto ml-1 text-blue-600" feature={FeatureType.SSO} />
<a
className="text-blue-600 text-sm hover:underline flex flex-row justify-center items-center ml-2"
@@ -72,8 +72,7 @@ const SSOSection = () => {
</a>
</div>
<Button
variant="outlined"
color="neutral"
variant="outline"
disabled={!isSSOFeatureEnabled}
onClick={() =>
setEditState({
@@ -88,14 +87,14 @@ const SSOSection = () => {
{identityProviderList.length > 0 && (
<div className="mt-2 flow-root">
<div className="overflow-x-auto">
<div className="inline-block border rounded-lg border-gray-300 dark:border-zinc-700 min-w-full align-middle">
<table className="min-w-full divide-y divide-gray-300 dark:divide-zinc-700">
<div className="inline-block border rounded-lg border-border min-w-full align-middle">
<table className="min-w-full divide-y divide-border">
<thead>
<tr>
<th scope="col" className="py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
<th scope="col" className="py-2 pl-4 pr-3 text-left text-sm font-semibold text-foreground">
ID
</th>
<th scope="col" className="px-3 py-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
<th scope="col" className="px-3 py-2 text-left text-sm font-semibold text-foreground">
Title
</th>
<th scope="col" className="relative py-2 pl-3 pr-4">
@@ -103,15 +102,15 @@ const SSOSection = () => {
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-zinc-800">
<tbody className="divide-y divide-border">
{identityProviderList.map((identityProvider) => (
<tr key={identityProvider.id}>
<td className="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-900 dark:text-gray-500">{identityProvider.id}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{identityProvider.title}</td>
<td className="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-foreground">{identityProvider.id}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-muted-foreground">{identityProvider.title}</td>
<td className="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm">
<IconButton
<Button
size="sm"
variant="plain"
variant="ghost"
onClick={() =>
setEditState({
open: true,
@@ -120,15 +119,10 @@ const SSOSection = () => {
}
>
<Icon.PenBox className="w-4 h-auto" />
</IconButton>
<IconButton
size="sm"
color="danger"
variant="plain"
onClick={() => handleDeleteIdentityProvider(identityProvider)}
>
</Button>
<Button size="sm" variant="ghost" onClick={() => handleDeleteIdentityProvider(identityProvider)}>
<Icon.Trash className="w-4 h-auto" />
</IconButton>
</Button>
</td>
</tr>
))}
@@ -1,8 +1,9 @@
import { Button, Option, Select, Textarea } from "@mui/joy";
import { head, isEqual } from "lodash-es";
import { useRef, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { workspaceServiceClient } from "@/grpcweb";
import { useWorkspaceStore } from "@/stores";
import { FeatureType } from "@/stores/workspace";
@@ -19,6 +20,16 @@ const getDefaultVisibility = (visibility?: Visibility) => {
return visibility;
};
const toVisibility = (value: string): Visibility => {
switch (value) {
case Visibility.PUBLIC.toString():
return Visibility.PUBLIC;
case Visibility.WORKSPACE.toString():
default:
return Visibility.WORKSPACE;
}
};
const convertFileToBase64 = (file: File) =>
new Promise<string>((resolve, reject) => {
const reader = new FileReader();
@@ -47,13 +58,6 @@ const WorkspaceGeneralSettingSection = () => {
setWorkspaceSetting({ ...workspaceSetting, branding: new TextEncoder().encode(base64) });
};
const handleCustomStyleChange = async (value: string) => {
setWorkspaceSetting({
...workspaceSetting,
customStyle: value,
});
};
const handleDefaultVisibilityChange = async (value: Visibility) => {
setWorkspaceSetting({
...workspaceSetting,
@@ -66,9 +70,6 @@ const WorkspaceGeneralSettingSection = () => {
if (!isEqual(originalWorkspaceSetting.current.branding, workspaceSetting.branding)) {
updateMask.push("branding");
}
if (!isEqual(originalWorkspaceSetting.current.customStyle, workspaceSetting.customStyle)) {
updateMask.push("custom_style");
}
if (!isEqual(originalWorkspaceSetting.current.defaultVisibility, workspaceSetting.defaultVisibility)) {
updateMask.push("default_visibility");
}
@@ -93,27 +94,27 @@ const WorkspaceGeneralSettingSection = () => {
return (
<div className="w-full flex flex-col sm:flex-row justify-start items-start gap-4 sm:gap-x-16">
<p className="sm:w-1/4 text-2xl shrink-0 font-semibold text-gray-900 dark:text-gray-500">General</p>
<p className="sm:w-1/4 text-2xl shrink-0 font-semibold text-foreground">General</p>
<div className="w-full sm:w-auto grow flex flex-col justify-start items-start gap-4">
<div className="w-full flex flex-row justify-between items-center">
<div className="w-full flex flex-col justify-start items-start">
<p className="flex flex-row justify-start items-center">
<span className="font-medium dark:text-gray-400">Custom branding</span>
<span className="font-medium text-foreground">Custom branding</span>
<FeatureBadge className="w-5 h-auto ml-1 text-blue-600" feature={FeatureType.CustomeBranding} />
</p>
<p className="text-sm text-gray-500 leading-tight">Recommand logo ratio: 1:1</p>
<p className="text-sm text-muted-foreground leading-tight">Recommand logo ratio: 1:1</p>
</div>
<div className="relative shrink-0 hover:opacity-80 flex flex-col items-end justify-center">
{branding ? (
<div className="relative w-12 h-12 mr-2">
<img src={branding} alt="branding" className="max-w-full max-h-full rounded-lg" />
<Icon.X
className="w-4 h-auto -top-2 -right-2 absolute z-10 border rounded-full bg-white opacity-80"
className="w-4 h-auto -top-2 -right-2 absolute z-10 border rounded-full bg-background opacity-80"
onClick={() => setWorkspaceSetting({ ...workspaceSetting, branding: new TextEncoder().encode("") })}
/>
</div>
) : (
<Icon.CircleSlash className="w-12 h-auto dark:text-gray-500 mr-2" strokeWidth={1} />
<Icon.CircleSlash className="w-12 h-auto text-muted-foreground mr-2" strokeWidth={1} />
)}
<input
className="absolute inset-0 z-1 opacity-0"
@@ -127,29 +128,22 @@ const WorkspaceGeneralSettingSection = () => {
</div>
<div className="w-full flex flex-row justify-between items-center">
<div className="w-full flex flex-col justify-start items-start">
<p className="font-medium dark:text-gray-400">{t("settings.workspace.default-visibility")}</p>
<p className="text-sm text-gray-500 leading-tight">The default visibility of new shortcuts/collections.</p>
<p className="font-medium text-foreground">{t("settings.workspace.default-visibility")}</p>
<p className="text-sm text-muted-foreground leading-tight">The default visibility of new shortcuts/collections.</p>
</div>
<Select
className="w-36"
defaultValue={getDefaultVisibility(workspaceSetting.defaultVisibility)}
onChange={(_, value) => handleDefaultVisibilityChange(value as Visibility)}
defaultValue={getDefaultVisibility(workspaceSetting.defaultVisibility).toString()}
onValueChange={(value) => handleDefaultVisibilityChange(toVisibility(value))}
>
<Option value={Visibility.WORKSPACE}>{t(`shortcut.visibility.workspace.self`)}</Option>
<Option value={Visibility.PUBLIC}>{t(`shortcut.visibility.public.self`)}</Option>
<SelectTrigger className="w-36">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={Visibility.WORKSPACE.toString()}>{t(`shortcut.visibility.workspace.self`)}</SelectItem>
<SelectItem value={Visibility.PUBLIC.toString()}>{t(`shortcut.visibility.public.self`)}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="w-full flex flex-col justify-start items-start">
<p className="mt-2 font-medium dark:text-gray-400">{t("settings.workspace.custom-style")}</p>
<Textarea
className="w-full mt-2"
placeholder="* {font-family: ui-monospace Monaco Consolas;}"
minRows={2}
maxRows={5}
value={workspaceSetting.customStyle}
onChange={(event) => handleCustomStyleChange(event.target.value)}
/>
</div>
<div>
<Button color="primary" disabled={!allowSave} onClick={handleSaveWorkspaceSetting}>
{t("common.save")}
@@ -1,10 +1,10 @@
import { Button, IconButton } from "@mui/joy";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { showCommonDialog } from "@/components/Alert";
import CreateUserDialog from "@/components/CreateUserDialog";
import Icon from "@/components/Icon";
import { Button } from "@/components/ui/button";
import { useUserStore } from "@/stores";
import { User } from "@/types/proto/api/v1/user_service";
@@ -43,12 +43,11 @@ const WorkspaceMembersSection = () => {
return (
<>
<div className="w-full flex flex-col sm:flex-row justify-start items-start gap-4 sm:gap-x-16">
<p className="sm:w-1/4 text-2xl shrink-0 font-semibold text-gray-900 dark:text-gray-500">{t("settings.workspace.member.self")}</p>
<p className="sm:w-1/4 text-2xl shrink-0 font-semibold text-foreground">{t("settings.workspace.member.self")}</p>
<div className="w-full sm:w-auto grow flex flex-col justify-start items-start gap-4">
<div className="w-full flex justify-end">
<Button
variant="outlined"
color="neutral"
variant="outline"
onClick={() => {
setShowCreateUserDialog(true);
setCurrentEditingUser(undefined);
@@ -59,17 +58,17 @@ const WorkspaceMembersSection = () => {
</div>
<div className="w-full flow-root">
<div className="overflow-x-auto">
<div className="inline-block border rounded-lg border-gray-300 dark:border-zinc-700 min-w-full align-middle">
<table className="min-w-full divide-y divide-gray-300 dark:divide-zinc-700">
<div className="inline-block border rounded-lg border-border min-w-full align-middle">
<table className="min-w-full divide-y divide-border">
<thead>
<tr>
<th scope="col" className="py-3 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
<th scope="col" className="py-3 pl-4 pr-3 text-left text-sm font-semibold text-foreground">
{t("user.nickname")}
</th>
<th scope="col" className="px-3 py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
<th scope="col" className="px-3 py-3 text-left text-sm font-semibold text-foreground">
{t("user.email")}
</th>
<th scope="col" className="px-3 py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
<th scope="col" className="px-3 py-3 text-left text-sm font-semibold text-foreground">
{t("user.role")}
</th>
<th scope="col" className="relative py-3 pl-3 pr-4">
@@ -77,26 +76,26 @@ const WorkspaceMembersSection = () => {
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-zinc-800">
<tbody className="divide-y divide-border">
{userList.map((user) => (
<tr key={user.email}>
<td className="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-900 dark:text-gray-500">{user.nickname}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.email}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.role}</td>
<td className="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-foreground">{user.nickname}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-muted-foreground">{user.email}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-muted-foreground">{user.role}</td>
<td className="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm">
<IconButton
<Button
size="sm"
variant="plain"
variant="ghost"
onClick={() => {
setCurrentEditingUser(user);
setShowCreateUserDialog(true);
}}
>
<Icon.PenBox className="w-4 h-auto" />
</IconButton>
<IconButton size="sm" color="danger" variant="plain" onClick={() => handleDeleteUser(user)}>
</Button>
<Button size="sm" variant="ghost" onClick={() => handleDeleteUser(user)}>
<Icon.Trash className="w-4 h-auto" />
</IconButton>
</Button>
</td>
</tr>
))}
@@ -1,6 +1,7 @@
import { Switch } from "@mui/joy";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { workspaceServiceClient } from "@/grpcweb";
import { useWorkspaceStore } from "@/stores";
import { WorkspaceSetting } from "@/types/proto/api/v1/workspace_service";
@@ -62,26 +63,28 @@ const WorkspaceSecuritySection = () => {
return (
<div className="w-full flex flex-col sm:flex-row justify-start items-start gap-4 sm:gap-x-16">
<p className="sm:w-1/4 text-2xl shrink-0 font-semibold text-gray-900 dark:text-gray-500">Security</p>
<p className="sm:w-1/4 text-2xl shrink-0 font-semibold text-foreground">Security</p>
<div className="w-full sm:w-auto grow flex flex-col justify-start items-start gap-4">
<SSOSection />
<div>
<div className="flex items-center space-x-2">
<Switch
className="dark:text-gray-500"
size="lg"
id="disallow-user-registration"
checked={workspaceStore.setting.disallowUserRegistration}
onChange={(event) => toggleDisallowUserRegistration(event.target.checked)}
endDecorator={<span>{t("settings.workspace.disallow-user-registration.self")}</span>}
onCheckedChange={toggleDisallowUserRegistration}
/>
<Label htmlFor="disallow-user-registration" className="text-foreground">
{t("settings.workspace.disallow-user-registration.self")}
</Label>
</div>
<div>
<div className="flex items-center space-x-2">
<Switch
className="dark:text-gray-500"
size="lg"
id="disallow-password-auth"
checked={workspaceStore.setting.disallowPasswordAuth}
onChange={(event) => toggleDisallowPasswordAuth(event.target.checked)}
endDecorator={<span>{"Disallow password auth"}</span>}
onCheckedChange={toggleDisallowPasswordAuth}
/>
<Label htmlFor="disallow-password-auth" className="text-foreground">
{"Disallow password auth"}
</Label>
</div>
</div>
</div>
@@ -0,0 +1,49 @@
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />);
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,95 @@
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import * as React from "react";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => <AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />);
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />);
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel ref={ref} className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)} {...props} />
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
+37
View File
@@ -0,0 +1,37 @@
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive: "border-destructive/50 text-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>>(
({ className, variant, ...props }, ref) => (
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
),
);
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => (
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />,
);
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };
+32
View File
@@ -0,0 +1,32 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<React.ElementRef<typeof AvatarPrimitive.Root>, React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>>(
({ className, ...props }, ref) => (
<AvatarPrimitive.Root ref={ref} className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)} {...props} />
),
);
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} />
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };
+28
View File
@@ -0,0 +1,28 @@
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
+42
View File
@@ -0,0 +1,42 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
});
Button.displayName = "Button";
export { Button, buttonVariants };
+36
View File
@@ -0,0 +1,36 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
));
CardFooter.displayName = "CardFooter";
export type CardProps = React.HTMLAttributes<HTMLDivElement>;
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,25 @@
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn("grid place-content-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };
+90
View File
@@ -0,0 +1,90 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
@@ -0,0 +1,174 @@
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} {...props} />
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />;
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};
+19
View File
@@ -0,0 +1,19 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
{...props}
/>
);
});
Input.displayName = "Input";
export { Input };
+14
View File
@@ -0,0 +1,14 @@
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />);
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
@@ -0,0 +1,35 @@
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { Circle } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem };
+133
View File
@@ -0,0 +1,133 @@
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
@@ -0,0 +1,19 @@
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as React from "react";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
+109
View File
@@ -0,0 +1,109 @@
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 flex flex-col overflow-hidden",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom: "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);
interface SheetContentProps extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
),
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left flex-shrink-0", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetBody = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex-1 overflow-y-auto min-h-0", className)} {...props} />
);
SheetBody.displayName = "SheetBody";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 pt-4 flex-shrink-0", className)} {...props} />
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetBody,
SheetFooter,
SheetTitle,
SheetDescription,
};
+35
View File
@@ -0,0 +1,35 @@
import { CircleCheck, Info, LoaderCircle, OctagonX, TriangleAlert } from "lucide-react";
import { useTheme } from "next-themes";
import { Toaster as Sonner } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheck className="h-4 w-4" />,
info: <Info className="h-4 w-4" />,
warning: <TriangleAlert className="h-4 w-4" />,
error: <OctagonX className="h-4 w-4" />,
loading: <LoaderCircle className="h-4 w-4 animate-spin" />,
}}
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
);
};
export { Toaster };
+26
View File
@@ -0,0 +1,26 @@
import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as React from "react";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
@@ -0,0 +1,18 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
{...props}
/>
);
});
Textarea.displayName = "Textarea";
export { Textarea };
@@ -0,0 +1,27 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as React from "react";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+75 -1
View File
@@ -2,10 +2,66 @@
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
body,
html,
#root {
@apply text-base w-full h-full dark:bg-zinc-900;
@apply text-base w-full h-full;
}
@layer utilities {
@@ -20,3 +76,21 @@ html,
scrollbar-width: none; /* Firefox */
}
}
@layer base {
h1 {
@apply text-4xl font-bold tracking-tight;
}
h2 {
@apply text-3xl font-semibold tracking-tight;
}
h3 {
@apply text-2xl font-semibold tracking-tight;
}
h4 {
@apply text-xl font-semibold tracking-tight;
}
p {
@apply leading-7;
}
}
-3
View File
@@ -1,3 +0,0 @@
.MuiDrawer-content {
@apply !w-auto;
}
+6 -6
View File
@@ -1,4 +1,4 @@
import { useColorScheme } from "@mui/joy";
import { useTheme } from "next-themes";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Outlet } from "react-router-dom";
@@ -9,7 +9,7 @@ import { useUserStore } from "@/stores";
const Root: React.FC = () => {
const navigateTo = useNavigateTo();
const { setMode } = useColorScheme();
const { setTheme } = useTheme();
const { i18n } = useTranslation();
const userStore = useUserStore();
const currentUser = userStore.getCurrentUser();
@@ -36,17 +36,17 @@ const Root: React.FC = () => {
i18n.changeLanguage(currentUserSetting.general?.locale || "en");
if (currentUserSetting.general?.colorTheme === "LIGHT") {
setMode("light");
setTheme("light");
} else if (currentUserSetting.general?.colorTheme === "DARK") {
setMode("dark");
setTheme("dark");
} else {
setMode("system");
setTheme("system");
}
}, [currentUserSetting]);
return (
isInitialized && (
<div className="w-full h-auto flex flex-col justify-start items-start dark:bg-zinc-900">
<div className="w-full h-auto flex flex-col justify-start items-start bg-background">
<Header />
<Navigator />
<Outlet />
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+5 -6
View File
@@ -1,9 +1,8 @@
import { CssVarsProvider } from "@mui/joy";
import { ThemeProvider } from "next-themes";
import { createRoot } from "react-dom/client";
import { Toaster } from "react-hot-toast";
import { RouterProvider } from "react-router-dom";
import { Toaster } from "@/components/ui/sonner";
import "./css/index.css";
import "./css/joy-ui.css";
import "./i18n";
import CommonContextProvider from "./layouts/CommonContextProvider";
import router from "./routers";
@@ -12,10 +11,10 @@ const container = document.getElementById("root");
const root = createRoot(container as HTMLElement);
root.render(
<CssVarsProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<CommonContextProvider>
<RouterProvider router={router} />
</CommonContextProvider>
<Toaster position="top-center" />
</CssVarsProvider>,
<Toaster />
</ThemeProvider>,
);
+3 -3
View File
@@ -4,14 +4,14 @@ import PasswordAuthForm from "@/components/PasswordAuthForm";
const AdminSignIn: React.FC = () => {
return (
<div className="flex flex-row justify-center items-center w-full h-auto pt-12 sm:pt-24 bg-white dark:bg-zinc-900">
<div className="flex flex-row justify-center items-center w-full h-auto pt-12 sm:pt-24 bg-background">
<div className="w-80 max-w-full h-full py-4 flex flex-col justify-start items-center">
<div className="w-full py-4 grow flex flex-col justify-center items-center">
<div className="flex flex-row justify-start items-center w-auto mx-auto gap-y-2 mb-4">
<Logo className="mr-2" />
<span className="text-3xl opacity-80 dark:text-gray-500">Slash</span>
<span className="text-3xl opacity-80 text-muted-foreground">Slash</span>
</div>
<p className="w-full text-xl font-medium dark:text-gray-500">Sign in with admin accounts</p>
<p className="w-full text-xl font-medium text-foreground">Sign in with admin accounts</p>
<PasswordAuthForm />
</div>
</div>
+1 -1
View File
@@ -69,7 +69,7 @@ const AuthCallback = () => {
return (
<div className="p-4 py-24 w-full h-full flex justify-center items-center">
{state.loading ? (
<Icon.Loader className="animate-spin dark:text-gray-200" />
<Icon.Loader className="animate-spin text-foreground" />
) : (
<div className="max-w-lg font-mono whitespace-pre-wrap opacity-80">{state.errorMessage}</div>
)}
@@ -1,4 +1,3 @@
import { Button, Input } from "@mui/joy";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import useLocalStorage from "react-use/lib/useLocalStorage";
@@ -6,6 +5,8 @@ import CollectionView from "@/components/CollectionView";
import CreateCollectionDrawer from "@/components/CreateCollectionDrawer";
import FilterView from "@/components/FilterView";
import Icon from "@/components/Icon";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import useLoading from "@/hooks/useLoading";
import { useShortcutStore, useCollectionStore } from "@/stores";
@@ -53,15 +54,13 @@ const CollectionDashboard: React.FC = () => {
<Input
className="w-32 mr-2"
type="text"
size="sm"
placeholder={t("common.search")}
startDecorator={<Icon.Search className="w-4 h-auto" />}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="flex flex-row justify-start items-center">
<Button className="hover:shadow" variant="soft" size="sm" onClick={() => setShowCreateCollectionDrawer(true)}>
<Button className="hover:shadow" variant="secondary" size="sm" onClick={() => setShowCreateCollectionDrawer(true)}>
<Icon.Plus className="w-5 h-auto" />
<span className="ml-0.5">{t("common.create")}</span>
</Button>
@@ -69,16 +68,16 @@ const CollectionDashboard: React.FC = () => {
</div>
<FilterView />
{loadingState.isLoading ? (
<div className="py-12 w-full flex flex-row justify-center items-center opacity-80 dark:text-gray-500">
<div className="py-12 w-full flex flex-row justify-center items-center opacity-80 text-muted-foreground">
<Icon.Loader className="mr-2 w-5 h-auto animate-spin" />
{t("common.loading")}
</div>
) : filteredCollections.length === 0 ? (
<div className="py-16 w-full flex flex-col justify-center items-center text-gray-400">
<div className="py-16 w-full flex flex-col justify-center items-center text-muted-foreground">
<Icon.PackageOpen size={64} strokeWidth={1} />
<p className="mt-2">No collections found.</p>
<a
className="text-blue-600 border-t text-sm hover:underline flex flex-row justify-center items-center mt-4 pt-2"
className="text-blue-600 border-t border-border text-sm hover:underline flex flex-row justify-center items-center mt-4 pt-2"
href="https://github.com/yourselfhosted/slash/blob/main/docs/getting-started/collections.md"
target="_blank"
>
+13 -15
View File
@@ -1,11 +1,11 @@
import { Divider } from "@mui/joy";
import classNames from "classnames";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useParams } from "react-router-dom";
import { toast } from "sonner";
import Icon from "@/components/Icon";
import ShortcutFrame from "@/components/ShortcutFrame";
import ShortcutView from "@/components/ShortcutView";
import { Separator } from "@/components/ui/separator";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import { useUserStore, useCollectionStore, useShortcutStore } from "@/stores";
import { Collection } from "@/types/proto/api/v1/collection_service";
@@ -65,26 +65,24 @@ const CollectionSpace = () => {
};
return (
<div className="w-full h-full sm:px-12 sm:py-10 sm:h-screen sm:bg-gray-100 dark:sm:bg-zinc-800">
<div className="w-full h-full flex flex-row sm:border dark:sm:border-zinc-800 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900">
<div className="w-full h-full sm:px-12 sm:py-10 sm:h-screen sm:bg-muted">
<div className="w-full h-full flex flex-row sm:border sm:border-border p-4 rounded-2xl bg-background">
<div className="w-full sm:w-56 sm:pr-4 flex flex-col justify-start items-start overflow-auto shrink-0">
<div className="w-full sticky top-0 px-2">
<div className="w-full flex flex-row justify-start items-center text-gray-800 dark:text-gray-300">
<div className="w-full flex flex-row justify-start items-center text-foreground">
<Icon.LibrarySquare className="w-5 h-auto mr-1 opacity-70 shrink-0" />
<span className="text-lg truncate">{collection.title}</span>
</div>
<p className="text-gray-500 text-sm truncate">{collection.description}</p>
<p className="text-muted-foreground text-sm truncate">{collection.description}</p>
</div>
<Divider className="!my-2" />
<Separator className="my-2" />
<div className="w-full flex flex-col justify-start items-start gap-2 sm:gap-1 px-px">
{shortcuts.map((shortcut) => {
return (
<ShortcutView
className={classNames(
"w-full py-2 cursor-pointer sm:!px-2",
selectedShortcut?.id === shortcut.id
? "bg-gray-100 dark:bg-zinc-800"
: "sm:border-transparent dark:sm:border-transparent",
selectedShortcut?.id === shortcut.id ? "bg-accent" : "sm:border-transparent",
)}
key={shortcut.name}
shortcut={shortcut}
@@ -96,16 +94,16 @@ const CollectionSpace = () => {
</div>
</div>
{sm && (
<div className="w-full h-full overflow-clip rounded-lg border dark:border-zinc-800 bg-white dark:bg-zinc-800">
<div className="w-full h-full overflow-clip rounded-lg border border-border bg-card">
{selectedShortcut ? (
<ShortcutFrame key={selectedShortcut.id} shortcut={selectedShortcut} />
) : (
<div className="w-full h-full flex flex-col justify-center items-center p-8">
<div className="w-72 max-w-full border dark:border-zinc-900 dark:bg-zinc-900 dark:text-gray-400 p-6 pb-4 rounded-2xl shadow-xl">
<div className="w-72 max-w-full border border-border bg-card text-muted-foreground p-6 pb-4 rounded-2xl shadow-xl">
<Icon.AppWindow className="w-12 h-auto mb-2 opacity-60" strokeWidth={1} />
<p className="text-lg font-medium">Click on a tab in the Sidebar to get started.</p>
<Divider className="!my-2" />
<p className="text-gray-400 dark:text-gray-600 text-sm mt-2 italic">
<p className="text-lg font-medium text-foreground">Click on a tab in the Sidebar to get started.</p>
<Separator className="my-2" />
<p className="text-muted-foreground text-sm mt-2 italic">
Shared by <span className="font-medium not-italic">{creator.nickname}</span>
</p>
</div>
+3 -3
View File
@@ -2,10 +2,10 @@ import Icon from "@/components/Icon";
const NotFound = () => {
return (
<div className="w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
<div className="w-full h-full overflow-y-auto overflow-x-hidden bg-muted">
<div className="w-full h-full flex flex-col justify-center items-center">
<Icon.Meh strokeWidth={1} className="w-20 h-auto opacity-80 dark:text-gray-300" />
<p className="mt-4 mb-8 text-4xl font-mono dark:text-gray-300">404</p>
<Icon.Meh strokeWidth={1} className="w-20 h-auto opacity-80 text-muted-foreground" />
<p className="mt-4 mb-8 text-4xl font-mono text-muted-foreground">404</p>
</div>
</div>
);
+8 -11
View File
@@ -1,4 +1,3 @@
import { Button, Input } from "@mui/joy";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import useLocalStorage from "react-use/lib/useLocalStorage";
@@ -7,7 +6,8 @@ import FilterView from "@/components/FilterView";
import Icon from "@/components/Icon";
import ShortcutsContainer from "@/components/ShortcutsContainer";
import ShortcutsNavigator from "@/components/ShortcutsNavigator";
import ViewSetting from "@/components/ViewSetting";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import useLoading from "@/hooks/useLoading";
import { useShortcutStore, useUserStore, useViewStore } from "@/stores";
import { getFilteredShortcutList, getOrderedShortcutList } from "@/stores/view";
@@ -49,21 +49,18 @@ const ShortcutDashboard: React.FC = () => {
<>
<div className="mx-auto max-w-8xl w-full px-4 sm:px-6 md:px-12 pt-4 pb-6 flex flex-col justify-start items-start">
<ShortcutsNavigator />
<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 gap-2">
<div className="flex flex-row justify-start items-center">
<Input
className="w-32 mr-2"
className="w-32"
type="text"
size="sm"
placeholder={t("common.search")}
startDecorator={<Icon.Search className="w-4 h-auto" />}
endDecorator={<ViewSetting />}
value={filter.search}
onChange={(e) => viewStore.setFilter({ search: e.target.value })}
/>
</div>
<div className="flex flex-row justify-end items-center">
<Button className="hover:shadow" variant="soft" size="sm" onClick={() => setShowCreateShortcutDrawer(true)}>
<Button className="hover:shadow" variant="secondary" size="sm" onClick={() => setShowCreateShortcutDrawer(true)}>
<Icon.Plus className="w-5 h-auto" />
<span className="ml-0.5">{t("common.create")}</span>
</Button>
@@ -71,16 +68,16 @@ const ShortcutDashboard: React.FC = () => {
</div>
<FilterView />
{loadingState.isLoading ? (
<div className="py-12 w-full flex flex-row justify-center items-center opacity-80 dark:text-gray-500">
<div className="py-12 w-full flex flex-row justify-center items-center opacity-80 text-muted-foreground">
<Icon.Loader className="mr-2 w-5 h-auto animate-spin" />
{t("common.loading")}
</div>
) : orderedShortcutList.length === 0 ? (
<div className="py-16 w-full flex flex-col justify-center items-center text-gray-400">
<div className="py-16 w-full flex flex-col justify-center items-center text-muted-foreground">
<Icon.PackageOpen size={64} strokeWidth={1} />
<p className="mt-2">No shortcuts found.</p>
<a
className="text-blue-600 border-t text-sm hover:underline flex flex-row justify-center items-center mt-4 pt-2"
className="text-blue-600 border-t border-border text-sm hover:underline flex flex-row justify-center items-center mt-4 pt-2"
href="https://github.com/yourselfhosted/slash/blob/main/docs/getting-started/shortcuts.md"
target="_blank"
>
+59 -44
View File
@@ -1,10 +1,9 @@
import { Tooltip } from "@mui/joy";
import classNames from "classnames";
import copy from "copy-to-clipboard";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { toast } from "sonner";
import { showCommonDialog } from "@/components/Alert";
import AnalyticsView from "@/components/AnalyticsView";
import CreateShortcutDrawer from "@/components/CreateShortcutDrawer";
@@ -13,6 +12,7 @@ import Icon from "@/components/Icon";
import LinkFavicon from "@/components/LinkFavicon";
import VisibilityIcon from "@/components/VisibilityIcon";
import Dropdown from "@/components/common/Dropdown";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { absolutifyLink } from "@/helpers/utils";
import useLoading from "@/hooks/useLoading";
import useNavigateTo from "@/hooks/useNavigateTo";
@@ -89,45 +89,51 @@ const ShortcutDetail = () => {
<div className="truncate text-3xl">
{shortcut.title ? (
<>
<span className="dark:text-gray-400">{shortcut.title}</span>
<span className="text-gray-500">(s/{shortcut.name})</span>
<span className="text-foreground">{shortcut.title}</span>
<span className="text-muted-foreground">(s/{shortcut.name})</span>
</>
) : (
<>
<span className="text-gray-400 dark:text-gray-500">s/</span>
<span className="truncate dark:text-gray-400">{shortcut.name}</span>
<span className="text-muted-foreground">s/</span>
<span className="truncate text-foreground">{shortcut.name}</span>
</>
)}
</div>
<span className="hidden group-hover:block ml-1 cursor-pointer shrink-0">
<Icon.ExternalLink className="w-6 h-auto text-gray-600" />
<Icon.ExternalLink className="w-6 h-auto text-muted-foreground" />
</span>
</a>
<div className="mt-2 w-full flex flex-row justify-normal items-center space-x-2">
<Tooltip title="Copy" variant="solid" placement="top" arrow>
<button
className="w-8 h-8 cursor-pointer border rounded-full text-gray-500 hover:bg-gray-100 hover:shadow dark:border-zinc-800 dark:hover:bg-zinc-800"
onClick={() => handleCopyButtonClick()}
>
<Icon.Clipboard className="w-4 h-auto mx-auto" />
</button>
<Tooltip>
<TooltipTrigger asChild>
<button
className="w-8 h-8 cursor-pointer border border-border rounded-full text-muted-foreground hover:bg-accent hover:shadow"
onClick={() => handleCopyButtonClick()}
>
<Icon.Clipboard className="w-4 h-auto mx-auto" />
</button>
</TooltipTrigger>
<TooltipContent>Copy</TooltipContent>
</Tooltip>
<Tooltip title="QR Code" variant="solid" placement="top" arrow>
<button
className="w-8 h-8 cursor-pointer border rounded-full text-gray-500 hover:bg-gray-100 hover:shadow dark:border-zinc-800 dark:hover:bg-zinc-800"
onClick={() => setShowQRCodeDialog(true)}
>
<Icon.QrCode className="w-4 h-auto mx-auto" />
</button>
<Tooltip>
<TooltipTrigger asChild>
<button
className="w-8 h-8 cursor-pointer border border-border rounded-full text-muted-foreground hover:bg-accent hover:shadow"
onClick={() => setShowQRCodeDialog(true)}
>
<Icon.QrCode className="w-4 h-auto mx-auto" />
</button>
</TooltipTrigger>
<TooltipContent>QR Code</TooltipContent>
</Tooltip>
{havePermission && (
<Dropdown
className="w-8 h-8 flex justify-center items-center border cursor-pointer rounded-full hover:bg-gray-100 hover:shadow dark:border-zinc-800 dark:hover:bg-zinc-800"
actionsClassName="!w-32 !-right-24 dark:text-gray-500"
className="w-8 h-8 flex justify-center items-center border border-border cursor-pointer rounded-full hover:bg-accent hover:shadow"
actionsClassName="!w-32 !-right-24"
actions={
<>
<button
className="w-full px-2 flex flex-row justify-start items-center text-left leading-8 cursor-pointer rounded hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60 dark:hover:bg-zinc-800"
className="w-full px-2 flex flex-row justify-start items-center text-left text-foreground leading-8 cursor-pointer rounded hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
onClick={() => {
setState({
...state,
@@ -138,7 +144,7 @@ const ShortcutDetail = () => {
<Icon.Edit className="w-4 h-auto mr-2" /> {t("common.edit")}
</button>
<button
className="w-full px-2 flex flex-row justify-start items-center text-left leading-8 cursor-pointer rounded text-red-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60 dark:hover:bg-zinc-800"
className="w-full px-2 flex flex-row justify-start items-center text-left leading-8 cursor-pointer rounded text-destructive hover:bg-accent disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-60"
onClick={() => {
handleDeleteShortcutButtonClick(shortcut);
}}
@@ -150,40 +156,49 @@ const ShortcutDetail = () => {
></Dropdown>
)}
</div>
{shortcut.description && <p className="w-full break-all mt-4 text-gray-500 dark:text-gray-400">{shortcut.description}</p>}
{shortcut.description && <p className="w-full break-all mt-4 text-muted-foreground">{shortcut.description}</p>}
<div className="mt-2 flex flex-row justify-start items-start flex-wrap gap-2">
{shortcut.tags.map((tag) => {
return (
<span key={tag} className="max-w-[8rem] truncate text-gray-400 text leading-4 dark:text-gray-500">
<span key={tag} className="max-w-[8rem] truncate text-muted-foreground text leading-4">
#{tag}
</span>
);
})}
{shortcut.tags.length === 0 && <span className="text-gray-400 text-sm leading-4 italic dark:text-gray-500">No tags</span>}
{shortcut.tags.length === 0 && <span className="text-muted-foreground text-sm leading-4 italic">No tags</span>}
</div>
<div className="w-full flex mt-4 gap-2">
<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 dark:border-zinc-800">
<Icon.User className="w-4 h-auto mr-1" />
<span className="max-w-[4rem] sm:max-w-[6rem] truncate">{creator.nickname}</span>
</div>
<Tooltip>
<TooltipTrigger asChild>
<div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border border-border rounded-full text-muted-foreground text-sm">
<Icon.User className="w-4 h-auto mr-1" />
<span className="max-w-[4rem] sm:max-w-[6rem] truncate">{creator.nickname}</span>
</div>
</TooltipTrigger>
<TooltipContent>Creator</TooltipContent>
</Tooltip>
<Tooltip title={t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.description`)} 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 dark:border-zinc-800">
<VisibilityIcon className="w-4 h-auto mr-1" visibility={shortcut.visibility} />
{t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.self`)}
</div>
<Tooltip>
<TooltipTrigger asChild>
<div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border border-border rounded-full text-muted-foreground text-sm">
<VisibilityIcon className="w-4 h-auto mr-1" visibility={shortcut.visibility} />
{t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.self`)}
</div>
</TooltipTrigger>
<TooltipContent>{t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.description`)}</TooltipContent>
</Tooltip>
<Tooltip title="View count" 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 dark:border-zinc-800">
<Icon.BarChart2 className="w-4 h-auto mr-1" />
{shortcut.viewCount} visits
</div>
<Tooltip>
<TooltipTrigger asChild>
<div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border border-border rounded-full text-muted-foreground text-sm">
<Icon.BarChart2 className="w-4 h-auto mr-1" />
{shortcut.viewCount} visits
</div>
</TooltipTrigger>
<TooltipContent>View count</TooltipContent>
</Tooltip>
</div>
<div className="w-full flex flex-col mt-8">
<h3 id="analytics" className="pl-1 font-medium text-lg flex flex-row justify-start items-center dark:text-gray-400">
<h3 id="analytics" className="pl-1 font-medium text-lg flex flex-row justify-start items-center text-foreground">
<Icon.BarChart2 className="w-6 h-auto mr-1" />
{t("analytics.self")}
</h3>
+3 -3
View File
@@ -1,8 +1,8 @@
import { Button } from "@mui/joy";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useParams, useSearchParams } from "react-router-dom";
import { toast } from "sonner";
import CreateShortcutDrawer from "@/components/CreateShortcutDrawer";
import { Button } from "@/components/ui/button";
import { isURL } from "@/helpers/utils";
import useNavigateTo from "@/hooks/useNavigateTo";
import { useShortcutStore, useUserStore } from "@/stores";
@@ -51,7 +51,7 @@ const ShortcutSpace = () => {
Shortcut <span className="font-mono">{shortcutName}</span> Not Found.
</p>
<div className="mt-4">
<Button variant="plain" size="sm" onClick={() => setShowCreateShortcutDrawer(true)}>
<Button variant="ghost" size="sm" onClick={() => setShowCreateShortcutDrawer(true)}>
👉 Click here to create it
</Button>
</div>

Some files were not shown because too many files have changed in this diff Show More