chore: update collection details

This commit is contained in:
Steven 2023-11-12 12:57:39 +08:00
parent 8de658709c
commit 8a4e07120f
8 changed files with 97 additions and 43 deletions

View File

@ -1,7 +1,10 @@
import classNames from "classnames"; import classNames from "classnames";
import copy from "copy-to-clipboard";
import { useState } from "react"; import { useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { absolutifyLink } from "@/helpers/utils";
import useNavigateTo from "@/hooks/useNavigateTo"; import useNavigateTo from "@/hooks/useNavigateTo";
import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import { useAppSelector } from "@/stores"; import { useAppSelector } from "@/stores";
@ -30,6 +33,11 @@ const CollectionView = (props: Props) => {
.map((shortcutId) => shortcutList.find((shortcut) => shortcut?.id === shortcutId)) .map((shortcutId) => shortcutList.find((shortcut) => shortcut?.id === shortcutId))
.filter(Boolean) as any as Shortcut[]; .filter(Boolean) as any as Shortcut[];
const handleCopyCollectionLink = () => {
copy(absolutifyLink(`/c/${collection.name}`));
toast.success("Collection link copied to clipboard.");
};
const handleDeleteCollectionButtonClick = () => { const handleDeleteCollectionButtonClick = () => {
showCommonDialog({ showCommonDialog({
title: "Delete Collection", title: "Delete Collection",
@ -49,10 +57,12 @@ const CollectionView = (props: Props) => {
<> <>
<div className={classNames("w-full flex flex-col justify-start items-start border rounded-lg hover:shadow dark:border-zinc-800")}> <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="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="w-auto flex flex-row justify-start items-center mr-1"> <div className="w-auto flex flex-col justify-start items-start mr-2">
<div className="truncate"> <div className="w-full truncate" onClick={handleCopyCollectionLink}>
<span className="dark:text-gray-400">{collection.title}</span> <span className="leading-6 font-medium dark:text-gray-400">{collection.title}</span>
<span className="ml-1 leading-6 text-gray-500 dark:text-gray-400">(c/{collection.name})</span>
</div> </div>
<p className="text-sm text-gray-500">{collection.description}</p>
</div> </div>
<div className="flex flex-row justify-end items-center shrink-0"> <div className="flex flex-row justify-end items-center shrink-0">
{collection.visibility !== Visibility.PRIVATE && ( {collection.visibility !== Visibility.PRIVATE && (
@ -86,7 +96,13 @@ const CollectionView = (props: Props) => {
<div className="w-full p-3 flex flex-row justify-start items-start flex-wrap gap-3"> <div className="w-full p-3 flex flex-row justify-start items-start flex-wrap gap-3">
{shortcuts.map((shortcut) => { {shortcuts.map((shortcut) => {
return ( return (
<ShortcutView key={shortcut.id} shortcut={shortcut} alwaysShowLink={!sm} onClick={() => handleShortcutClick(shortcut)} /> <ShortcutView
key={shortcut.id}
className="!w-auto"
shortcut={shortcut}
alwaysShowLink={!sm}
onClick={() => handleShortcutClick(shortcut)}
/>
); );
})} })}
</div> </div>

View File

@ -35,9 +35,9 @@ const CreateCollectionDialog: React.FC<Props> = (props: Props) => {
const [selectedShortcuts, setSelectedShortcuts] = useState<Shortcut[]>([]); const [selectedShortcuts, setSelectedShortcuts] = useState<Shortcut[]>([]);
const requestState = useLoading(false); const requestState = useLoading(false);
const isCreating = isUndefined(collectionId); const isCreating = isUndefined(collectionId);
const unselectedShortcuts = shortcutList.filter( const unselectedShortcuts = shortcutList
(shortcut) => !selectedShortcuts.find((selectedShortcut) => selectedShortcut.id === shortcut.id) .filter((shortcut) => (state.collectionCreate.visibility === Visibility.PUBLIC ? shortcut.visibility === "PUBLIC" : true))
); .filter((shortcut) => !selectedShortcuts.find((selectedShortcut) => selectedShortcut.id === shortcut.id));
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@ -100,8 +100,8 @@ const CreateCollectionDialog: React.FC<Props> = (props: Props) => {
}; };
const handleSaveBtnClick = async () => { const handleSaveBtnClick = async () => {
if (!state.collectionCreate.name) { if (!state.collectionCreate.name || !state.collectionCreate.title) {
toast.error("Name is required"); toast.error("Please fill in required fields.");
return; return;
} }
@ -147,24 +147,28 @@ const CreateCollectionDialog: React.FC<Props> = (props: Props) => {
</div> </div>
<div className="overflow-y-auto overflow-x-hidden w-80 sm:w-96 max-w-full"> <div className="overflow-y-auto overflow-x-hidden w-80 sm:w-96 max-w-full">
<div className="w-full flex flex-col justify-start items-start mb-3"> <div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">Name</span> <span className="mb-2">
Name <span className="text-red-600">*</span>
</span>
<div className="relative w-full"> <div className="relative w-full">
<Input <Input
className="w-full" className="w-full"
type="text" type="text"
placeholder="Unique collection name" placeholder="Should be an unique name and will be put in url"
value={state.collectionCreate.name} value={state.collectionCreate.name}
onChange={handleNameInputChange} onChange={handleNameInputChange}
/> />
</div> </div>
</div> </div>
<div className="w-full flex flex-col justify-start items-start mb-3"> <div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">Title</span> <span className="mb-2">
Title <span className="text-red-600">*</span>
</span>
<div className="relative w-full"> <div className="relative w-full">
<Input <Input
className="w-full" className="w-full"
type="text" type="text"
placeholder="Title" placeholder="A short title to describe your collection"
value={state.collectionCreate.title} value={state.collectionCreate.title}
onChange={handleTitleInputChange} onChange={handleTitleInputChange}
/> />
@ -176,7 +180,7 @@ const CreateCollectionDialog: React.FC<Props> = (props: Props) => {
<Input <Input
className="w-full" className="w-full"
type="text" type="text"
placeholder="Description" placeholder="A slightly longer description"
value={state.collectionCreate.description} value={state.collectionCreate.description}
onChange={handleDescriptionInputChange} onChange={handleDescriptionInputChange}
/> />
@ -200,12 +204,12 @@ const CreateCollectionDialog: React.FC<Props> = (props: Props) => {
<span className="opacity-60">({selectedShortcuts.length})</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>} {selectedShortcuts.length === 0 && <span className="ml-2 italic opacity-80 text-sm">Select a shortcut first</span>}
</p> </p>
<div className="w-full py-1 flex flex-row justify-start items-start flex-wrap overflow-hidden gap-2"> <div className="w-full py-1 px-px flex flex-row justify-start items-start flex-wrap overflow-hidden gap-2">
{selectedShortcuts.map((shortcut) => { {selectedShortcuts.map((shortcut) => {
return ( return (
<ShortcutView <ShortcutView
key={shortcut.id} key={shortcut.id}
className="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-gray-100 shadow dark:bg-zinc-800 dark:border-zinc-700 dark:text-gray-400"
shortcut={shortcut} shortcut={shortcut}
onClick={() => { onClick={() => {
setSelectedShortcuts([...selectedShortcuts.filter((selectedShortcut) => selectedShortcut.id !== shortcut.id)]); setSelectedShortcuts([...selectedShortcuts.filter((selectedShortcut) => selectedShortcut.id !== shortcut.id)]);
@ -217,7 +221,7 @@ const CreateCollectionDialog: React.FC<Props> = (props: Props) => {
return ( return (
<ShortcutView <ShortcutView
key={shortcut.id} key={shortcut.id}
className="border-dashed" className="!w-auto select-none max-w-[40%] border-dashed cursor-pointer"
shortcut={shortcut} shortcut={shortcut}
onClick={() => { onClick={() => {
setSelectedShortcuts([...selectedShortcuts, shortcut]); setSelectedShortcuts([...selectedShortcuts, shortcut]);
@ -225,6 +229,12 @@ const CreateCollectionDialog: React.FC<Props> = (props: Props) => {
/> />
); );
})} })}
{selectedShortcuts.length + unselectedShortcuts.length === 0 && (
<div className="w-full flex flex-row justify-center items-center text-gray-400">
<Icon.PackageOpen className="w-6 h-auto" />
<p className="ml-2">No shortcuts found.</p>
</div>
)}
</div> </div>
</div> </div>

View File

@ -163,8 +163,8 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
}; };
const handleSaveBtnClick = async () => { const handleSaveBtnClick = async () => {
if (!state.shortcutCreate.name) { if (!state.shortcutCreate.name || !state.shortcutCreate.link) {
toast.error("Name is required"); toast.error("Please fill in required fields.");
return; return;
} }
@ -209,19 +209,23 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
</div> </div>
<div className="overflow-y-auto overflow-x-hidden"> <div className="overflow-y-auto overflow-x-hidden">
<div className="w-full flex flex-col justify-start items-start mb-3"> <div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">Name</span> <span className="mb-2">
Name <span className="text-red-600">*</span>
</span>
<div className="relative w-full"> <div className="relative w-full">
<Input <Input
className="w-full" className="w-full"
type="text" type="text"
placeholder="Unique shortcut name" placeholder="Should be an unique name and will be put in url"
value={state.shortcutCreate.name} value={state.shortcutCreate.name}
onChange={handleNameInputChange} onChange={handleNameInputChange}
/> />
</div> </div>
</div> </div>
<div className="w-full flex flex-col justify-start items-start mb-3"> <div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">Destination URL</span> <span className="mb-2">
Destination URL <span className="text-red-600">*</span>
</span>
<Input <Input
className="w-full" className="w-full"
type="text" type="text"

View File

@ -28,10 +28,10 @@ const Header: React.FC = () => {
return ( return (
<> <>
<div className="w-full bg-gray-50 dark:bg-zinc-800 border-b border-b-gray-200 dark:border-b-zinc-800"> <div className="w-full bg-gray-50 dark:bg-zinc-800 border-b border-b-gray-200 dark:border-b-zinc-800">
<div className="w-full max-w-8xl mx-auto px-3 md:px-12 py-3 sm:py-5 flex flex-row justify-between items-center"> <div className="w-full max-w-8xl mx-auto px-3 md:px-12 py-3 flex flex-row justify-between items-center">
<div className="flex flex-row justify-start items-center shrink mr-2"> <div className="flex flex-row justify-start items-center shrink mr-2">
<Link to="/" className="sm:text-lg cursor-pointer flex flex-row justify-start items-center dark:text-gray-400"> <Link to="/" className="cursor-pointer flex flex-row justify-start items-center dark:text-gray-400">
<img id="logo-img" src="/logo.png" className="w-6 sm:w-8 h-auto mr-2 -mt-0.5 dark:opacity-80 rounded-full shadow" alt="" /> <img id="logo-img" src="/logo.png" className="w-7 h-auto mr-2 -mt-0.5 dark:opacity-80 rounded-full shadow" alt="" />
Slash Slash
</Link> </Link>
{profile.plan === PlanType.PRO && ( {profile.plan === PlanType.PRO && (
@ -41,10 +41,10 @@ const Header: React.FC = () => {
)} )}
{shouldShowRouterSwitch && ( {shouldShowRouterSwitch && (
<> <>
<span className="font-mono opacity-60 mx-2">/</span> <span className="font-mono opacity-60 mx-1">/</span>
<Dropdown <Dropdown
trigger={ trigger={
<button className="sm:text-lg flex flex-row justify-end items-center cursor-pointer"> <button className="flex flex-row justify-end items-center cursor-pointer">
<span className="dark:text-gray-400">{location.pathname === "/" ? "Shortcuts" : "Collections"}</span> <span className="dark:text-gray-400">{location.pathname === "/" ? "Shortcuts" : "Collections"}</span>
<Icon.ChevronsUpDown className="ml-1 w-4 h-auto text-gray-600 dark:text-gray-400" /> <Icon.ChevronsUpDown className="ml-1 w-4 h-auto text-gray-600 dark:text-gray-400" />
</button> </button>
@ -56,13 +56,13 @@ const Header: React.FC = () => {
to="/" to="/"
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 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"
> >
<Icon.SquareSlash className="w-5 h-auto mr-2" /> Shortcuts <Icon.SquareSlash className="w-5 h-auto mr-2 opacity-70" /> Shortcuts
</Link> </Link>
<Link <Link
to="/collections" to="/collections"
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 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"
> >
<Icon.LibrarySquare className="w-5 h-auto mr-2" /> Collections <Icon.LibrarySquare className="w-5 h-auto mr-2 opacity-70" /> Collections
</Link> </Link>
</> </>
} }

View File

@ -24,13 +24,13 @@ const ShortcutView = (props: Props) => {
)} )}
onClick={onClick} onClick={onClick}
> >
<Link to={`/shortcut/${shortcut.id}`} className={classNames("w-5 h-5 flex justify-center items-center overflow-clip shrink-0")}> <div className={classNames("w-5 h-5 flex justify-center items-center overflow-clip shrink-0")}>
{favicon ? ( {favicon ? (
<img className="w-full h-auto rounded-lg" src={favicon} decoding="async" loading="lazy" /> <img className="w-full h-auto rounded-lg" src={favicon} decoding="async" loading="lazy" />
) : ( ) : (
<Icon.CircleSlash className="w-full h-auto text-gray-400" /> <Icon.CircleSlash className="w-full h-auto text-gray-400" />
)} )}
</Link> </div>
<div className="ml-1 w-full truncate"> <div className="ml-1 w-full truncate">
{shortcut.title ? ( {shortcut.title ? (
<> <>

View File

@ -1,4 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import { absolutifyLink } from "@/helpers/utils";
import useViewStore from "../stores/v1/view"; import useViewStore from "../stores/v1/view";
import ShortcutCard from "./ShortcutCard"; import ShortcutCard from "./ShortcutCard";
import ShortcutView from "./ShortcutView"; import ShortcutView from "./ShortcutView";
@ -13,6 +14,10 @@ const ShortcutsContainer: React.FC<Props> = (props: Props) => {
const displayStyle = viewStore.displayStyle || "full"; const displayStyle = viewStore.displayStyle || "full";
const ShortcutItemView = viewStore.displayStyle === "compact" ? ShortcutView : ShortcutCard; const ShortcutItemView = viewStore.displayStyle === "compact" ? ShortcutView : ShortcutCard;
const handleShortcutClick = (shortcut: Shortcut) => {
window.open(absolutifyLink(`/s/${shortcut.id}`));
};
return ( return (
<div <div
className={classNames( className={classNames(
@ -21,7 +26,7 @@ const ShortcutsContainer: React.FC<Props> = (props: Props) => {
)} )}
> >
{shortcutList.map((shortcut) => { {shortcutList.map((shortcut) => {
return <ShortcutItemView key={shortcut.id} shortcut={shortcut} showActions={true} />; return <ShortcutItemView key={shortcut.id} shortcut={shortcut} showActions={true} onClick={() => handleShortcutClick(shortcut)} />;
})} })}
</div> </div>
); );

View File

@ -38,6 +38,19 @@ const CollectionDashboard: React.FC = () => {
return ( return (
<> <>
<div className="mx-auto max-w-8xl w-full px-3 md:px-12 pt-4 pb-6 flex flex-col justify-start items-start"> <div className="mx-auto max-w-8xl w-full px-3 md:px-12 pt-4 pb-6 flex flex-col justify-start items-start">
<div className="w-full flex flex-row justify-start items-start mb-4">
<div className="bg-yellow-100 dark:bg-yellow-500 dark:opacity-70 py-2 px-3 rounded-full border dark:border-yellow-600 flex flex-row justify-start items-center cursor-pointer shadow">
<Icon.LibrarySquare className="w-5 h-auto opacity-60" />
<a
className="hover:underline hover:text-blue-600"
href="https://github.com/boojack/slash/blob/main/docs/getting-started/collections.md"
target="_blank"
>
<span className="mx-1 text-sm">Collection is in Beta. Learn more in docs</span>
<Icon.ExternalLink className="w-4 h-auto inline-block" />
</a>
</div>
</div>
<div className="w-full flex flex-row justify-between items-center mb-4"> <div className="w-full flex flex-row justify-between items-center mb-4">
<div className="flex flex-row justify-start items-center"> <div className="flex flex-row justify-start items-center">
<Button className="hover:shadow" variant="soft" size="sm" onClick={() => setShowCreateCollectionDialog(true)}> <Button className="hover:shadow" variant="soft" size="sm" onClick={() => setShowCreateCollectionDialog(true)}>

View File

@ -1,6 +1,7 @@
import { Divider } from "@mui/joy"; import { Divider } from "@mui/joy";
import classNames from "classnames"; import classNames from "classnames";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import Icon from "@/components/Icon"; import Icon from "@/components/Icon";
import ShortcutView from "@/components/ShortcutView"; import ShortcutView from "@/components/ShortcutView";
@ -26,6 +27,7 @@ const CollectionSpace = () => {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try {
const collection = await collectionStore.fetchCollectionByName(collectionName); const collection = await collectionStore.fetchCollectionByName(collectionName);
setCollection(collection); setCollection(collection);
setShortcuts([]); setShortcuts([]);
@ -39,6 +41,10 @@ const CollectionSpace = () => {
// do nth // do nth
} }
} }
} catch (error: any) {
console.error(error);
toast.error(error.details);
}
})(); })();
}, [collectionName]); }, [collectionName]);