refactor: shadcn UI (#496)

* feat: add shadcn/ui base dependencies

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: configure shadcn/ui setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: add cn utility for class merging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: configure tailwind with shadcn theme and CSS variables

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: add shadcn button component

* feat: add shadcn input component

* feat: add shadcn label component

* feat: add shadcn card component

* feat: add shadcn badge component

* feat: add shadcn separator component

* chore: update dependencies for separator component

* feat: add shadcn dialog component

* feat: add shadcn sheet component (drawer replacement)

* feat: add shadcn dropdown-menu component

* feat: add shadcn tooltip component

* feat: add shadcn checkbox component

* feat: add shadcn textarea component

* feat: add shadcn avatar component

* feat: add shadcn sonner component (toast replacement)

* refactor: migrate Dropdown to shadcn dropdown-menu

* refactor: migrate Alert to shadcn alert-dialog

* refactor: migrate ShortcutCard to shadcn components

* refactor: migrate CreateShortcutDrawer to shadcn sheet

* fix: correct import path in alert-dialog

* feat: add framer-motion for animations

* feat: add animated card with framer motion

* feat: improve typography with consistent styles

* chore: migrate MUI Joy components to shadcn/ui (partial)

- Migrated core app components (main.tsx, App.tsx, Root.tsx)
- Replaced CssVarsProvider with next-themes ThemeProvider
- Replaced useColorScheme with useTheme from next-themes
- Migrated all page components using Button, Input, Divider, Tooltip, Separator
- Migrated component files using simple components
- Migrated setting sections with Select, Switch, Textarea
- Added shadcn/ui components: alert, switch, select, radio-group
- Build tested and passing

Remaining: Dialog/Modal, Drawer/Sheet, RadioGroup components in 10 files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor: complete Task 26 - migrate remaining components from MUI Joy to shadcn/ui

Migrated 11 remaining files using MUI Joy components to shadcn/ui:

Dialog migrations (6 files):
- AboutDialog.tsx: Modal/ModalDialog → Dialog
- ChangePasswordDialog.tsx: Modal/ModalDialog → Dialog
- CreateAccessTokenDialog.tsx: Modal/ModalDialog → Dialog with RadioGroup
- CreateUserDialog.tsx: Modal/ModalDialog → Dialog with RadioGroup
- EditUserinfoDialog.tsx: Modal/ModalDialog → Dialog
- GenerateQRCodeDialog.tsx: Modal/ModalDialog → Dialog

Drawer migrations (2 files):
- CreateCollectionDrawer.tsx: Drawer → Sheet
- CreateIdentityProviderDrawer.tsx: Drawer → Sheet

Page migrations (2 files):
- SubscriptionSetting.tsx: Alert, Button, Divider, Link, Textarea → shadcn equivalents
- WorkspaceSetting.tsx: Alert, Button, Divider → shadcn equivalents

Component migrations (1 file):
- SubscriptionFAQ.tsx: Accordion/AccordionGroup → shadcn Accordion

All components now use shadcn/ui. No MUI Joy imports remain.
Build verified successfully.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* chore: remove MUI Joy and react-hot-toast dependencies

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: add UI components guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: complete migration from MUI Joy to shadcn/ui

- Migrated all UI components to shadcn/ui
- Implemented clean & minimal design aesthetic
- Added Framer Motion for subtle animations
- Improved typography and spacing consistency
- Enhanced dark mode with proper color palette
- Replaced react-hot-toast with sonner
- Removed MUI Joy dependencies
- Updated all forms, modals, and interactive components
- Added component documentation

This migration improves maintainability, reduces bundle size, and provides a more modern, polished UI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* chore: update

* chore: update

* chore: update

* refactor: complete shadcn/ui theme migration with semantic tokens

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Johnny <yourselfhosted@gmail.com>
This commit is contained in:
boojack
2025-12-18 18:06:00 +08:00
committed by GitHub
co-authored by Claude Sonnet 4.5 Johnny
parent 0825e0bac0
commit c38b58618c
80 changed files with 5851 additions and 1672 deletions
File diff suppressed because it is too large Load Diff
+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
+21 -4
View File
@@ -9,24 +9,39 @@
"type-check": "tsc --noEmit --skipLibCheck"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/joy": "5.0.0-beta.48",
"@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",
"framer-motion": "^12.23.26",
"i18next": "^24.2.3",
"lodash-es": "^4.17.21",
"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.5.2",
"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"
@@ -35,6 +50,7 @@
"@bufbuild/protobuf": "^2.6.3",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/lodash-es": "^4.17.12",
"@types/node": "^25.0.3",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/uuid": "^10.0.0",
@@ -50,6 +66,7 @@
"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"
},
+1246 -496
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,4 +1,4 @@
import { useColorScheme } from "@mui/joy";
import { useTheme } from "next-themes";
import { useEffect } from "react";
import { Outlet } from "react-router-dom";
import { useWorkspaceStore } from "@/stores";
@@ -7,7 +7,7 @@ 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.
+23 -15
View File
@@ -1,5 +1,6 @@
import { Button, Link, Modal, ModalDialog } from "@mui/joy";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import Icon from "./Icon";
interface Props {
@@ -11,28 +12,35 @@ 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 className="flex flex-row justify-between items-center">
<span>{t("common.about")}</span>
<Button variant="ghost" size="icon" onClick={onClose}>
<Icon.X className="w-5 h-auto" />
</Button>
</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;
+37 -33
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,49 +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 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.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 dark:divide-zinc-800">
<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 dark:text-gray-500">
{device.name || "Unknown"}
<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>
<span className="whitespace-nowrap py-2 pr-2 text-sm text-gray-500 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,7 +1,10 @@
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";
@@ -54,7 +57,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 +66,36 @@ 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 className="flex flex-row justify-between items-center">
<span>Change Password</span>
<Button variant="ghost" size="icon" onClick={handleCloseBtnClick}>
<Icon.X className="w-5 h-auto" />
</Button>
</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={handleCloseBtnClick}>
{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>
);
};
+37 -23
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, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { absolutifyLink } from "@/helpers/utils";
import useNavigateTo from "@/hooks/useNavigateTo";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
@@ -62,37 +62,51 @@ 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>
<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>
<TooltipProvider>
<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>
</TooltipProvider>
<TooltipProvider>
<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>
</TooltipProvider>
{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 +114,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,7 +1,11 @@
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";
@@ -55,9 +59,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 +89,57 @@ 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 className="flex flex-row justify-between items-center">
<span>Create Access Token</span>
<Button variant="ghost" size="icon" onClick={onClose}>
<Icon.X className="w-5 h-auto" />
</Button>
</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, SheetContent, SheetDescription, 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,77 +162,79 @@ 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>
<Sheet open={true} onOpenChange={onClose}>
<SheetContent className="w-full sm:max-w-md overflow-y-auto">
<SheetHeader>
<SheetTitle>{isCreating ? "Create Collection" : "Edit Collection"}</SheetTitle>
<SheetDescription>{isCreating ? "Create a new collection of shortcuts" : "Edit your collection details"}</SheetDescription>
</SheetHeader>
<div className="mt-6 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"
startDecorator="c/"
placeholder="An easy name to remember"
value={state.collectionCreate.name}
onChange={handleNameInputChange}
placeholder="A short title of your collection"
value={state.collectionCreate.title}
onChange={handleTitleInputChange}
/>
</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">
<Input
className="w-full"
type="text"
placeholder="A short title of your collection"
value={state.collectionCreate.title}
onChange={handleTitleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
type="text"
placeholder="A slightly longer description"
value={state.collectionCreate.description}
onChange={handleDescriptionInputChange}
/>
</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">
<Input
className="w-full"
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">
<div className="flex items-center space-x-2">
<Checkbox
className="w-full dark:text-gray-400"
id="public"
checked={state.collectionCreate.visibility === Visibility.PUBLIC}
label={t(`shortcut.visibility.public.description`)}
onChange={(e) =>
onCheckedChange={(checked) =>
setPartialState({
collectionCreate: Object.assign(state.collectionCreate, {
visibility: e.target.checked ? Visibility.PUBLIC : Visibility.WORKSPACE,
visibility: checked ? Visibility.PUBLIC : Visibility.WORKSPACE,
}),
})
}
/>
<Label htmlFor="public" className="text-sm font-normal cursor-pointer">
{t(`shortcut.visibility.public.description`)}
</Label>
</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">
<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-gray-100 shadow dark:bg-zinc-800 dark:border-zinc-700 dark:text-gray-400"
className="!w-auto select-none max-w-[40%] cursor-pointer bg-muted shadow"
shortcut={shortcut}
onClick={() => {
setSelectedShortcuts([...selectedShortcuts.filter((selectedShortcut) => selectedShortcut.id !== shortcut.id)]);
@@ -248,7 +255,7 @@ const CreateCollectionDrawer: React.FC<Props> = (props: Props) => {
);
})}
{selectedShortcuts.length + unselectedShortcuts.length === 0 && (
<div className="w-full flex flex-row justify-center items-center text-gray-400">
<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>
@@ -256,18 +263,16 @@ const CreateCollectionDrawer: 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}>
<SheetFooter className="mt-6">
<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, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { workspaceServiceClient } from "@/grpcweb";
import { absolutifyLink } from "@/helpers/utils";
import useLoading from "@/hooks/useLoading";
@@ -132,159 +136,160 @@ 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">
<Input
className="w-full"
type="text"
placeholder="A short title will be displayed in the UI"
value={state.identityProviderCreate.title}
onChange={handleTitleInputChange}
/>
</div>
<Sheet open={true} onOpenChange={onClose}>
<SheetContent className="w-full sm:max-w-md overflow-y-auto">
<SheetHeader>
<SheetTitle>{isCreating ? "Create Identity Provider" : "Edit Identity Provider"}</SheetTitle>
<SheetDescription>
{isCreating ? "Configure a new OAuth2 identity provider" : "Edit your identity provider settings"}
</SheetDescription>
</SheetHeader>
<div className="mt-6 space-y-4">
<div className="space-y-2">
<Label htmlFor="title">
Title <span className="text-destructive">*</span>
</Label>
<Input
id="title"
type="text"
placeholder="A short title will be displayed in the UI"
value={state.identityProviderCreate.title}
onChange={handleTitleInputChange}
/>
</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">
<Input
className="w-full"
type="text"
placeholder="Client ID of the OAuth2 provider"
value={state.identityProviderCreate.config?.oauth2?.clientId}
onChange={(e) => handleOAuth2ConfigChange(e, "clientId")}
/>
</div>
<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="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">
<Input
className="w-full"
type="text"
placeholder="Client Secret of the OAuth2 provider"
value={state.identityProviderCreate.config?.oauth2?.clientSecret}
onChange={(e) => handleOAuth2ConfigChange(e, "clientSecret")}
/>
</div>
<div className="space-y-2">
<Label htmlFor="client-id">
Client ID <span className="text-destructive">*</span>
</Label>
<Input
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 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">
<Input
className="w-full"
type="text"
placeholder="Authorization endpoint of the OAuth2 provider"
value={state.identityProviderCreate.config?.oauth2?.authUrl}
onChange={(e) => handleOAuth2ConfigChange(e, "authUrl")}
/>
</div>
<div className="space-y-2">
<Label htmlFor="client-secret">
Client Secret <span className="text-destructive">*</span>
</Label>
<Input
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 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">
<Input
className="w-full"
type="text"
placeholder="Token endpoint of the OAuth2 provider"
value={state.identityProviderCreate.config?.oauth2?.tokenUrl}
onChange={(e) => handleOAuth2ConfigChange(e, "tokenUrl")}
/>
</div>
<div className="space-y-2">
<Label htmlFor="auth-url">
Authorization endpoint <span className="text-destructive">*</span>
</Label>
<Input
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 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">
<Input
className="w-full"
type="text"
placeholder="User endpoint of the OAuth2 provider"
value={state.identityProviderCreate.config?.oauth2?.userInfoUrl}
onChange={(e) => handleOAuth2ConfigChange(e, "userInfoUrl")}
/>
</div>
<div className="space-y-2">
<Label htmlFor="token-url">
Token endpoint <span className="text-destructive">*</span>
</Label>
<Input
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 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">
<Input
className="w-full"
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 className="space-y-2">
<Label htmlFor="user-info-url">
User endpoint <span className="text-destructive">*</span>
</Label>
<Input
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>
<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">
<Input
className="w-full"
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 className="space-y-2">
<Label htmlFor="scopes">
Scopes <span className="text-destructive">*</span>
</Label>
<Input
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 className="w-full flex flex-col justify-start items-start">
<span className="mb-2">Display name</span>
<div className="relative w-full">
<Input
className="w-full"
type="text"
placeholder="The field in the user info response to display the user"
value={state.identityProviderCreate.config?.oauth2?.fieldMapping?.displayName}
onChange={(e) => handleFieldMappingChange(e, "displayName")}
/>
</div>
<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
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 className="space-y-2">
<Label htmlFor="display-name">Display name</Label>
<Input
id="display-name"
type="text"
placeholder="The field in the user info response to display the user"
value={state.identityProviderCreate.config?.oauth2?.fieldMapping?.displayName}
onChange={(e) => handleFieldMappingChange(e, "displayName")}
/>
</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}>
<SheetFooter className="mt-6">
<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, SheetContent, SheetDescription, 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,71 +204,80 @@ 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}
/>
<Sheet open={true} onOpenChange={onClose}>
<SheetContent className="w-full sm:max-w-md overflow-y-auto">
<SheetHeader>
<SheetTitle>{isCreating ? "Create Shortcut" : "Edit Shortcut"}</SheetTitle>
<SheetDescription>{isCreating ? "Create a new shortcut" : "Edit your shortcut details"}</SheetDescription>
</SheetHeader>
<div className="mt-6 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">
<span className="mb-2">
Link <span className="text-red-600">*</span>
</span>
<div className="space-y-2">
<Label htmlFor="link">
Link <span className="text-destructive">*</span>
</Label>
<Input
className="w-full"
id="link"
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>
<div className="space-y-2">
<Label htmlFor="title">Title</Label>
<Input
className="w-full"
id="title"
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>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Input
className="w-full"
id="description"
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} />
<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="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">
<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-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"
className="text-muted-foreground cursor-pointer text-sm hover:text-foreground transition-colors"
key={tag}
onClick={() => handleTagSuggestionsClick(tag)}
>
@@ -271,68 +288,76 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
</div>
)}
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<div className="flex items-center space-x-2">
<Checkbox
className="w-full dark:text-gray-400"
id="public"
checked={state.shortcutCreate.visibility === Visibility.PUBLIC}
label={t(`shortcut.visibility.public.description`)}
onChange={(e) =>
onCheckedChange={(checked) =>
setPartialState({
shortcutCreate: Object.assign(state.shortcutCreate, {
visibility: e.target.checked ? Visibility.PUBLIC : Visibility.WORKSPACE,
visibility: checked ? Visibility.PUBLIC : Visibility.WORKSPACE,
}),
})
}
/>
<Label htmlFor="public" className="text-sm font-normal cursor-pointer">
{t(`shortcut.visibility.public.description`)}
</Label>
</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">
<Separator className="my-4" />
<div className="border rounded-lg overflow-hidden">
<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" : "",
"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 flex-row justify-start items-center">
<span className="text-sm flex items-center gap-1">
Social media metadata
<Icon.Sparkles className="w-4 h-auto shrink-0 ml-1 text-blue-600 dark:text-blue-500" />
<Icon.Sparkles className="w-4 h-auto text-primary" />
</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>
<Icon.ChevronDown
className={classnames("w-4 h-auto text-muted-foreground transition-transform", showOpenGraphMetadata && "rotate-180")}
/>
</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>
<div className="p-3 space-y-4">
<div className="space-y-2">
<Label htmlFor="og-image" className="text-sm">
Image URL
</Label>
<Input
className="w-full"
id="og-image"
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>
<div className="space-y-2">
<Label htmlFor="og-title" className="text-sm">
Title
</Label>
<Input
className="w-full"
id="og-title"
type="text"
placeholder="Slash - An open source, self-hosted platform for sharing and managing your most frequently used links"
size="sm"
placeholder="Slash - An open source, self-hosted platform"
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>
<div className="space-y-2">
<Label htmlFor="og-description" className="text-sm">
Description
</Label>
<Textarea
className="w-full"
id="og-description"
placeholder="An open source, self-hosted platform for sharing and managing your most frequently used links."
size="sm"
maxRows={3}
rows={3}
value={state.shortcutCreate.ogMetadata?.description}
onChange={handleOpenGraphMetadataDescriptionChange}
/>
@@ -341,18 +366,17 @@ const CreateShortcutDrawer: React.FC<Props> = (props: Props) => {
)}
</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}>
<SheetFooter className="mt-6">
<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,8 +1,12 @@
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";
@@ -78,10 +82,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 +131,35 @@ 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 className="flex flex-row justify-between items-center">
<span>{isCreating ? "Create User" : "Edit User"}</span>
<Button variant="ghost" size="icon" onClick={onClose}>
<Icon.X className="w-5 h-auto" />
</Button>
</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 +167,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,7 +1,10 @@
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";
@@ -50,7 +53,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 +62,36 @@ 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 className="flex flex-row justify-between items-center">
<span>Edit Userinfo</span>
<Button variant="ghost" size="icon" onClick={handleCloseBtnClick}>
<Icon.X className="w-5 h-auto" />
</Button>
</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={handleCloseBtnClick}>
{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,8 +1,9 @@
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 Icon from "./Icon";
@@ -37,27 +38,29 @@ const GenerateQRCodeDialog: React.FC<Props> = (props: Props) => {
};
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 className="flex flex-row justify-between items-center">
<span>QR Code</span>
<Button variant="ghost" size="icon" onClick={handleCloseBtnClick}>
<Icon.X className="w-5 h-auto" />
</Button>
</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>
);
};
+14 -14
View File
@@ -29,40 +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">
<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);
}}
+118 -98
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 { AnimatedCard } from "@/components/ui/animated-card";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Tooltip, TooltipContent, TooltipProvider, 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,108 +38,125 @@ 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",
)}
>
<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")}
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="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",
)}
target="_blank"
href={shortcutLink}
>
<div className="truncate">
<span className="dark:text-gray-400">{shortcut.title}</span>
{shortcut.title ? (
<span className="text-gray-500">({shortcut.name})</span>
) : (
<span className="truncate dark:text-gray-400">{shortcut.name}</span>
<TooltipProvider>
<AnimatedCard
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 rounded")}
to={`/shortcut/${shortcut.id}`}
viewTransition
>
<LinkFavicon url={shortcut.link} />
</Link>
<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 hover:opacity-80 hover:underline transition-all",
)}
</div>
<span className="hidden group-hover:block ml-1 cursor-pointer shrink-0">
<Icon.ExternalLink className="w-4 h-auto text-gray-600" />
</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()}
target="_blank"
href={shortcutLink}
>
<Icon.Clipboard className="w-4 h-auto mx-auto" />
</button>
</Tooltip>
<div className="truncate">
<span className="text-foreground font-medium">{shortcut.title}</span>
{shortcut.title ? (
<span className="text-muted-foreground ml-1">({shortcut.name})</span>
) : (
<span className="truncate text-foreground font-medium">{shortcut.name}</span>
)}
</div>
<span className="hidden group-hover:block ml-1 shrink-0">
<Icon.ExternalLink className="w-4 h-auto text-muted-foreground" />
</span>
</a>
<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-muted-foreground hover:underline transition-all"
href={shortcut.link}
target="_blank"
>
{shortcut.link}
</a>
</div>
<a
className="pr-4 leading-tight w-full text-sm truncate text-gray-400 dark:text-gray-500 hover:underline"
href={shortcut.link}
target="_blank"
>
{shortcut.link}
</a>
</div>
<div className="h-full pt-2 flex flex-row justify-end items-start">
<ShortcutActionsDropdown shortcut={shortcut} />
</div>
</div>
<div className="h-full pt-2 flex flex-row justify-end items-start">
<ShortcutActionsDropdown shortcut={shortcut} />
<div className="mt-3 w-full flex flex-row justify-start items-start gap-2 truncate">
{shortcut.tags.map((tag) => {
return (
<Badge
key={tag}
variant="secondary"
className="max-w-[8rem] truncate cursor-pointer hover:bg-secondary/80 transition-colors"
onClick={() => viewStore.setFilter({ tag: tag })}
>
#{tag}
</Badge>
);
})}
{shortcut.tags.length === 0 && <span className="text-muted-foreground text-sm italic">No tags</span>}
</div>
</div>
<div className="mt-2 w-full flex flex-row justify-start items-start gap-2 truncate">
{shortcut.tags.map((tag) => {
return (
<span
key={tag}
className="max-w-[8rem] truncate text-gray-400 dark:text-gray-500 text-sm leading-4 cursor-pointer hover:opacity-80"
onClick={() => viewStore.setFilter({ tag: tag })}
>
#{tag}
</span>
);
})}
{shortcut.tags.length === 0 && <span className="text-gray-400 text-sm leading-4 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>
</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>
<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>
</div>
</div>
<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>
<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>
<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>
</AnimatedCard>
</TooltipProvider>
);
};
@@ -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>
+15 -46
View File
@@ -1,5 +1,6 @@
import { ReactNode, useEffect, useRef, useState } from "react";
import { ReactNode } from "react";
import Icon from "@/components/Icon";
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
interface Props {
trigger?: ReactNode;
@@ -10,54 +11,22 @@ 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);
};
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>
<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">
{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,10 @@
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 { Textarea } from "@/components/ui/textarea";
import { workspaceServiceClient } from "@/grpcweb";
import { useWorkspaceStore } from "@/stores";
import { FeatureType } from "@/stores/workspace";
@@ -19,6 +21,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();
@@ -93,27 +105,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,25 +139,27 @@ 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>
<p className="mt-2 font-medium text-foreground">{t("settings.workspace.custom-style")}</p>
<Textarea
className="w-full mt-2"
className="w-full mt-2 min-h-[80px]"
placeholder="* {font-family: ui-monospace Monaco Consolas;}"
minRows={2}
maxRows={5}
value={workspaceSetting.customStyle}
onChange={(event) => handleCustomStyleChange(event.target.value)}
/>
@@ -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 };
@@ -0,0 +1,22 @@
import { motion } from "framer-motion";
import { forwardRef } from "react";
import { Card, CardProps } from "@/components/ui/card";
const AnimatedCard = forwardRef<HTMLDivElement, CardProps>(({ className, children, ...props }, ref) => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, ease: "easeOut" }}
whileHover={{ y: -2 }}
>
<Card ref={ref} className={className} {...props}>
{children}
</Card>
</motion.div>
);
});
AnimatedCard.displayName = "AnimatedCard";
export { AnimatedCard };
+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-10 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 };
+92
View File
@@ -0,0 +1,92 @@
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",
{
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", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", 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, 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 dark:border-t-zinc-600 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>
);
+6 -9
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";
@@ -54,16 +54,13 @@ const ShortcutDashboard: React.FC = () => {
<Input
className="w-32 mr-2"
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 dark:border-t-zinc-600 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"
>
+74 -49
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, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { absolutifyLink } from "@/helpers/utils";
import useLoading from "@/hooks/useLoading";
import useNavigateTo from "@/hooks/useNavigateTo";
@@ -89,45 +89,55 @@ 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>
<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>
<TooltipProvider>
<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>
</TooltipProvider>
<TooltipProvider>
<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>
</TooltipProvider>
{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 +148,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 +160,55 @@ 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>
<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>
<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>
<TooltipProvider>
<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>
</TooltipProvider>
<TooltipProvider>
<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>
</TooltipProvider>
<TooltipProvider>
<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>
</TooltipProvider>
</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>
+15 -11
View File
@@ -1,10 +1,11 @@
import { Button, Divider } from "@mui/joy";
import React from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import Logo from "@/components/Logo";
import PasswordAuthForm from "@/components/PasswordAuthForm";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { absolutifyLink } from "@/helpers/utils";
import { useWorkspaceStore } from "@/stores";
import { IdentityProvider, IdentityProvider_Type } from "@/types/proto/api/v1/workspace_service";
@@ -32,37 +33,40 @@ const SignIn: 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 text-foreground opacity-80">Slash</span>
</div>
{!workspaceStore.setting.disallowPasswordAuth ? (
<PasswordAuthForm />
) : (
<p className="w-full text-2xl mt-2 dark:text-gray-500">Password auth is not allowed.</p>
<p className="w-full text-2xl mt-2 text-muted-foreground">Password auth is not allowed.</p>
)}
{!workspaceStore.setting.disallowUserRegistration && !workspaceStore.setting.disallowPasswordAuth && (
<p className="w-full mt-4 text-sm">
<span className="dark:text-gray-500">{"Don't have an account yet?"}</span>
<Link className="cursor-pointer ml-2 text-blue-600 hover:underline" to="/auth/signup" viewTransition>
<span className="text-muted-foreground">{"Don't have an account yet?"}</span>
<Link className="cursor-pointer ml-2 text-primary hover:underline" to="/auth/signup" viewTransition>
{t("auth.sign-up")}
</Link>
</p>
)}
{workspaceStore.setting.identityProviders.length > 0 && (
<>
<Divider className="!my-4">{t("common.or")}</Divider>
<div className="w-full flex items-center my-4">
<Separator className="flex-1" />
<span className="px-2 text-sm text-muted-foreground">{t("common.or")}</span>
<Separator className="flex-1" />
</div>
<div className="w-full flex flex-col space-y-2">
{workspaceStore.setting.identityProviders.map((identityProvider) => (
<Button
key={identityProvider.id}
variant="outlined"
color="neutral"
variant="outline"
className="w-full"
size="md"
size="default"
onClick={() => handleSignInWithIdentityProvider(identityProvider)}
>
{t("auth.sign-in-with", { provider: identityProvider.title })}
+13 -14
View File
@@ -1,9 +1,10 @@
import { Button, Input } from "@mui/joy";
import React, { FormEvent, useEffect, 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 Logo from "@/components/Logo";
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";
@@ -71,18 +72,18 @@ const SignUp: 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 text-foreground opacity-80">Slash</span>
</div>
<p className="w-full text-2xl mt-6 dark:text-gray-500">{t("auth.create-your-account")}</p>
<p className="w-full text-2xl mt-6 text-foreground">{t("auth.create-your-account")}</p>
<form className="w-full mt-4" onSubmit={handleSignupBtnClick}>
<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"
@@ -92,11 +93,11 @@ const SignUp: React.FC = () => {
/>
</div>
<div className="w-full flex flex-col mb-2">
<span className="leading-8 text-gray-600">Nickname</span>
<span className="leading-8 text-muted-foreground">Nickname</span>
<Input className="w-full py-3" type="text" value={nickname} placeholder="slash" onChange={handleNicknameInputChanged} />
</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>
@@ -104,21 +105,19 @@ const SignUp: React.FC = () => {
<Button
className="w-full"
type="submit"
color="primary"
loading={actionBtnLoadingState.isLoading}
disabled={actionBtnLoadingState.isLoading || !allowConfirm}
onClick={handleSignupBtnClick}
>
{t("auth.sign-up")}
{actionBtnLoadingState.isLoading ? "Loading..." : t("auth.sign-up")}
</Button>
</div>
</form>
{!workspaceStore.profile.owner ? (
<p className="w-full mt-4 text-sm font-medium dark:text-gray-500">{t("auth.host-tip")}</p>
<p className="w-full mt-4 text-sm font-medium text-muted-foreground">{t("auth.host-tip")}</p>
) : (
<p className="w-full mt-4 text-sm">
<span className="dark:text-gray-500">{"Already has an account?"}</span>
<Link className="cursor-pointer ml-2 text-blue-600 hover:underline" to="/auth" viewTransition>
<span className="text-muted-foreground">{"Already has an account?"}</span>
<Link className="cursor-pointer ml-2 text-primary hover:underline" to="/auth" viewTransition>
{t("auth.sign-in")}
</Link>
</p>
+70 -56
View File
@@ -1,9 +1,12 @@
import { Alert, Button, Divider, Link, Textarea } from "@mui/joy";
import { useState } from "react";
import toast from "react-hot-toast";
import { toast } from "sonner";
import { showCommonDialog } from "@/components/Alert";
import Icon from "@/components/Icon";
import SubscriptionFAQ from "@/components/SubscriptionFAQ";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { subscriptionServiceClient } from "@/grpcweb";
import { useUserStore, useWorkspaceStore } from "@/stores";
import { stringifyPlanType } from "@/stores/subscription";
@@ -26,7 +29,7 @@ const SubscriptionSetting: React.FC = () => {
showCommonDialog({
title: "Reset licence key",
content: `Are you sure to reset the license key? You cannot undo this action.`,
style: "warning",
style: "danger",
onConfirm: async () => {
try {
await subscriptionServiceClient.deleteSubscription({});
@@ -60,15 +63,14 @@ const SubscriptionSetting: React.FC = () => {
return (
<div className="mx-auto max-w-8xl w-full px-4 sm:px-6 md:px-12 pt-8 pb-24 flex flex-col justify-start items-start gap-y-12">
<div className="w-full">
<p className="text-2xl shrink-0 font-semibold text-gray-900 dark:text-gray-500">Subscription</p>
<p className="text-2xl shrink-0 font-semibold text-foreground">Subscription</p>
<div className="mt-2">
<span className="text-gray-500 mr-2">Current plan:</span>
<span className="text-2xl mr-4 dark:text-gray-400">{stringifyPlanType(subscription.plan)}</span>
<span className="text-muted-foreground mr-2">Current plan:</span>
<span className="text-2xl mr-4 text-foreground">{stringifyPlanType(subscription.plan)}</span>
</div>
<Textarea
className="w-full mt-2"
minRows={2}
maxRows={2}
rows={2}
placeholder="Enter your license key here - write only"
value={licenseKey}
onChange={(event) => setLicenseKey(event.target.value)}
@@ -76,15 +78,20 @@ const SubscriptionSetting: React.FC = () => {
<div className="w-full flex justify-between items-center mt-4">
<div>
{subscription.plan === PlanType.FREE && (
<Link href="https://yourselfhosted.gumroad.com/l/slash-pro" target="_blank">
<a
href="https://yourselfhosted.gumroad.com/l/slash-pro"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center"
>
Buy a license key
<Icon.ExternalLink className="w-4 h-auto ml-1" />
</Link>
</a>
)}
</div>
<div className="flex justify-end items-center gap-2">
{subscription.plan !== PlanType.FREE && (
<Button color="neutral" variant="plain" onClick={handleDeleteLicenseKey}>
<Button variant="outline" onClick={handleDeleteLicenseKey}>
Reset
</Button>
)}
@@ -94,135 +101,142 @@ const SubscriptionSetting: React.FC = () => {
</div>
</div>
</div>
<Divider />
<section className="w-full pb-8 dark:bg-zinc-900 flex items-center justify-center">
<Separator />
<section className="w-full pb-8 bg-background flex items-center justify-center">
<div className="w-full px-6">
<div className="max-w-4xl mx-auto mb-12">
<Alert className="!inline-block mb-12">
Slash is an open source, self-hosted platform for sharing and managing your most frequently used links. Easily create
customizable, human-readable shortcuts to streamline your link management. Our source code is available and accessible on{" "}
<Link href="https://github.com/yourselfhosted/slash" target="_blank">
GitHub
</Link>{" "}
so anyone can get it, inspect it and review it.
<Alert className="inline-block mb-12">
<AlertDescription>
Slash is an open source, self-hosted platform for sharing and managing your most frequently used links. Easily create
customizable, human-readable shortcuts to streamline your link management. Our source code is available and accessible on{" "}
<a
href="https://github.com/yourselfhosted/slash"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
GitHub
</a>{" "}
so anyone can get it, inspect it and review it.
</AlertDescription>
</Alert>
</div>
<div className="w-full grid grid-cols-1 gap-6 lg:gap-12 mt-8 md:grid-cols-3 md:max-w-4xl mx-auto">
<div className="flex flex-col p-6 bg-white dark:bg-zinc-800 shadow-lg rounded-lg justify-between border border-gray-300 dark:border-zinc-700">
<div className="flex flex-col p-6 bg-card shadow-lg rounded-lg justify-between border border-border">
<div>
<h3 className="text-2xl font-bold text-center dark:text-gray-300">Free</h3>
<div className="mt-3 text-center text-zinc-600 dark:text-zinc-400">
<h3 className="text-2xl font-bold text-center text-card-foreground">Free</h3>
<div className="mt-3 text-center text-muted-foreground">
<span className="text-4xl font-bold">$0</span>/ month
</div>
<ul className="mt-4 space-y-3">
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.CheckCircle2 className="w-5 h-auto text-green-600 mr-1 shrink-0" />
Full API access
</li>
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.CheckCircle2 className="w-5 h-auto text-green-600 mr-1 shrink-0" />
Browser extension
</li>
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.CheckCircle2 className="w-5 h-auto text-green-600 mr-1 shrink-0" />
Basic analytics
</li>
<li className="flex items-center dark:text-gray-300">
<Icon.AlertCircle className="w-5 h-auto text-gray-400 mr-1 shrink-0" />
<li className="flex items-center text-card-foreground">
<Icon.AlertCircle className="w-5 h-auto text-muted-foreground mr-1 shrink-0" />
Up to 100 shortcuts
</li>
<li className="flex items-center dark:text-gray-300">
<Icon.AlertCircle className="w-5 h-auto text-gray-400 mr-1 shrink-0" />
<li className="flex items-center text-card-foreground">
<Icon.AlertCircle className="w-5 h-auto text-muted-foreground mr-1 shrink-0" />
Up to 5 collections
</li>
<li className="flex items-center dark:text-gray-300">
<Icon.AlertCircle className="w-5 h-auto text-gray-400 mr-1 shrink-0" />
<li className="flex items-center text-card-foreground">
<Icon.AlertCircle className="w-5 h-auto text-muted-foreground mr-1 shrink-0" />
Up to 5 members
</li>
</ul>
</div>
</div>
<div className="relative flex flex-col p-6 bg-white dark:bg-zinc-800 shadow-lg rounded-lg dark:bg-zinc-850 justify-between border border-purple-500">
<div className="relative flex flex-col p-6 bg-card shadow-lg rounded-lg justify-between border border-purple-500">
<div className="px-3 py-1 text-sm text-white bg-gradient-to-r from-pink-500 to-purple-500 rounded-full inline-block absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
Popular
</div>
<div>
<h3 className="text-2xl font-bold text-center dark:text-gray-300">Pro</h3>
<div className="mt-3 text-center text-zinc-600 dark:text-zinc-400">
<h3 className="text-2xl font-bold text-center text-card-foreground">Pro</h3>
<div className="mt-3 text-center text-muted-foreground">
<span className="text-4xl font-bold">$4</span>/ month
</div>
<p className="mt-3 font-medium dark:text-gray-300">Everything in Free, and</p>
<p className="mt-3 font-medium text-card-foreground">Everything in Free, and</p>
<ul className="mt-4 space-y-3">
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.CheckCircle2 className="w-5 h-auto text-green-600 mr-1 shrink-0" />
Unlimited members
</li>
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.CheckCircle2 className="w-5 h-auto text-green-600 mr-1 shrink-0" />
Unlimited shortcuts
</li>
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.CheckCircle2 className="w-5 h-auto text-green-600 mr-1 shrink-0" />
Unlimited collections
</li>
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.CheckCircle2 className="w-5 h-auto text-green-600 mr-1 shrink-0" />
High-priority in roadmap
</li>
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.CheckCircle2 className="w-5 h-auto text-green-600 mr-1 shrink-0" />
Email support
</li>
</ul>
</div>
<div className="mt-6">
<Link className="w-full" underline="none" href="https://yourselfhosted.gumroad.com/l/slash-pro" target="_blank">
<a className="w-full block" href="https://yourselfhosted.gumroad.com/l/slash-pro" target="_blank" rel="noopener noreferrer">
<Button className="w-full bg-gradient-to-r from-pink-500 to-purple-500 shadow hover:opacity-80">Get Pro License</Button>
</Link>
</a>
</div>
</div>
<div className="flex flex-col p-6 bg-white dark:bg-zinc-800 shadow-lg rounded-lg dark:bg-zinc-850 justify-between border border-gray-300 dark:border-zinc-700">
<div className="flex flex-col p-6 bg-card shadow-lg rounded-lg justify-between border border-border">
<div>
<span className="block text-2xl text-center dark:text-gray-200 opacity-80">Team</span>
<div className="mt-3 text-center text-zinc-600 dark:text-zinc-400">
<span className="block text-2xl text-center text-card-foreground opacity-80">Team</span>
<div className="mt-3 text-center text-muted-foreground">
<span className="mr-2">start with</span>
<span>
<span className="text-4xl font-bold">$10</span>/ month
</span>
</div>
<p className="mt-3 font-medium dark:text-gray-300">Everything in Pro, and</p>
<p className="mt-3 font-medium text-card-foreground">Everything in Pro, and</p>
<ul className="mt-4 space-y-3">
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.BadgeCheck className="w-5 h-auto text-green-600 mr-1 shrink-0" />
Custom branding
</li>
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.BarChart3 className="w-5 h-auto text-green-600 mr-1 shrink-0" />
Advanced analytics
</li>
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.Shield className="w-5 h-auto text-green-600 mr-1 shrink-0" />
Single Sign-On(SSO)
</li>
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.Building2 className="w-5 h-auto text-green-600 mr-1 shrink-0" />
Priority support
</li>
<li className="flex items-center dark:text-gray-300">
<li className="flex items-center text-card-foreground">
<Icon.Sparkles className="w-5 h-auto text-green-600 mr-1 shrink-0" />
More coming soon
</li>
</ul>
</div>
<div className="mt-6">
<Link
className="w-full"
underline="none"
<a
className="w-full block"
href="mailto:yourselfhosted@gmail.com?subject=Inquiry about Slash Team Plan"
target="_blank"
rel="noopener noreferrer"
>
<Button className="w-full">Contact us</Button>
</Link>
</a>
</div>
</div>
</div>
+14 -10
View File
@@ -1,10 +1,12 @@
import { Alert, Button, Divider } from "@mui/joy";
import { useEffect } from "react";
import { Link } from "react-router-dom";
import Icon from "@/components/Icon";
import WorkspaceGeneralSettingSection from "@/components/setting/WorkspaceGeneralSettingSection";
import WorkspaceMembersSection from "@/components/setting/WorkspaceMembersSection";
import WorkspaceSecuritySection from "@/components/setting/WorkspaceSecuritySection";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { useUserStore, useWorkspaceStore } from "@/stores";
import { stringifyPlanType } from "@/stores/subscription";
import { Role } from "@/types/proto/api/v1/user_service";
@@ -26,26 +28,28 @@ const WorkspaceSetting = () => {
return (
<div className="mx-auto max-w-8xl w-full px-4 sm:px-6 md:px-12 py-6 flex flex-col justify-start items-start gap-y-12">
<Alert variant="soft" color="warning" startDecorator={<Icon.Info />}>
You can see the settings items below because you are an Admin.
<Alert variant="default" className="border-primary bg-muted">
<Icon.Info className="h-4 w-4" />
<AlertDescription>You can see the settings items below because you are an Admin.</AlertDescription>
</Alert>
<div className="w-full flex flex-col">
<p className="text-2xl shrink-0 font-semibold text-gray-900 dark:text-gray-500">Subscription</p>
<p className="text-2xl shrink-0 font-semibold text-foreground">Subscription</p>
<div className="mt-2">
<span className="text-gray-500 mr-2">Current plan:</span>
<span className="text-2xl mr-4 dark:text-gray-400">{stringifyPlanType(workspaceStore.getSubscription().plan)}</span>
<span className="text-muted-foreground mr-2">Current plan:</span>
<span className="text-2xl mr-4 text-foreground">{stringifyPlanType(workspaceStore.getSubscription().plan)}</span>
<Link to="/setting/subscription" viewTransition>
<Button size="sm" variant="outlined" startDecorator={<Icon.Settings className="w-4 h-auto" />}>
<Button size="sm" variant="outline">
<Icon.Settings className="w-4 h-auto mr-2" />
Manage
</Button>
</Link>
</div>
</div>
<Divider />
<Separator />
<WorkspaceMembersSection />
<Divider />
<Separator />
<WorkspaceGeneralSettingSection />
<Divider />
<Separator />
<WorkspaceSecuritySection />
</div>
);
+102 -27
View File
@@ -1,33 +1,108 @@
/* eslint-disable no-undef */
const { fontFamily } = require("tailwindcss/defaultTheme");
module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,tsx}"],
darkMode: "class",
darkMode: ["class", "class"],
theme: {
fontSize: {
xs: ".75rem",
sm: ".875rem",
base: "1rem",
lg: "1.125rem",
xl: "1.25rem",
"2xl": "1.5rem",
"3xl": "1.875rem",
"4xl": "2.25rem",
},
extend: {
maxWidth: {
"8xl": "88rem",
},
spacing: {
112: "28rem",
128: "32rem",
180: "45rem",
},
zIndex: {
1: "1",
20: "20",
100: "100",
1000: "1000",
},
},
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
fontSize: {
xs: '.75rem',
sm: '.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem'
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
fontFamily: {
sans: fontFamily.sans
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
},
maxWidth: {
'8xl': '88rem'
},
spacing: {
'112': '28rem',
'128': '32rem',
'180': '45rem'
},
zIndex: {
'1': '1',
'20': '20',
'100': '100',
'1000': '1000'
}
}
},
plugins: [require("tailwindcss-animate")],
};