Fix merge conflicts
This commit is contained in:
@@ -8,12 +8,12 @@ import ProfilePhoto from "./ProfilePhoto";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||
import useAccountStore from "@/store/account";
|
||||
import EditCollectionModal from "./ModalContent/EditCollectionModal";
|
||||
import EditCollectionSharingModal from "./ModalContent/EditCollectionSharingModal";
|
||||
import DeleteCollectionModal from "./ModalContent/DeleteCollectionModal";
|
||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
|
||||
export default function CollectionCard({
|
||||
collection,
|
||||
@@ -22,7 +22,7 @@ export default function CollectionCard({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { settings } = useLocalSettingsStore();
|
||||
const { account } = useAccountStore();
|
||||
const { data: user = {} } = useUser();
|
||||
|
||||
const formattedDate = new Date(collection.createdAt as string).toLocaleString(
|
||||
"en-US",
|
||||
@@ -41,18 +41,18 @@ export default function CollectionCard({
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOwner = async () => {
|
||||
if (collection && collection.ownerId !== account.id) {
|
||||
if (collection && collection.ownerId !== user.id) {
|
||||
const owner = await getPublicUserData(collection.ownerId as number);
|
||||
setCollectionOwner(owner);
|
||||
} else if (collection && collection.ownerId === account.id) {
|
||||
} else if (collection && collection.ownerId === user.id) {
|
||||
setCollectionOwner({
|
||||
id: account.id as number,
|
||||
name: account.name,
|
||||
username: account.username,
|
||||
image: account.image,
|
||||
archiveAsScreenshot: account.archiveAsScreenshot,
|
||||
archiveAsMonolith: account.archiveAsMonolith,
|
||||
archiveAsPDF: account.archiveAsPDF,
|
||||
id: user.id as number,
|
||||
name: user.name,
|
||||
username: user.username as string,
|
||||
image: user.image as string,
|
||||
archiveAsScreenshot: user.archiveAsScreenshot as boolean,
|
||||
archiveAsMonolith: user.archiveAsMonolith as boolean,
|
||||
archiveAsPDF: user.archiveAsPDF as boolean,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -76,7 +76,7 @@ export default function CollectionCard({
|
||||
>
|
||||
<i className="bi-three-dots text-xl" title="More"></i>
|
||||
</div>
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-52 mt-1">
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box mt-1">
|
||||
{permissions === true && (
|
||||
<li>
|
||||
<div
|
||||
@@ -86,6 +86,7 @@ export default function CollectionCard({
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setEditCollectionModal(true);
|
||||
}}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("edit_collection_info")}
|
||||
</div>
|
||||
@@ -99,6 +100,7 @@ export default function CollectionCard({
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setEditCollectionSharingModal(true);
|
||||
}}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{permissions === true
|
||||
? t("share_and_collaborate")
|
||||
@@ -113,6 +115,7 @@ export default function CollectionCard({
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setDeleteCollectionModal(true);
|
||||
}}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{permissions === true
|
||||
? t("delete_collection")
|
||||
@@ -155,11 +158,9 @@ export default function CollectionCard({
|
||||
<Link
|
||||
href={`/collections/${collection.id}`}
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(45deg, ${collection.color}30 10%, ${
|
||||
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
|
||||
} 50%, ${
|
||||
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
|
||||
} 100%)`,
|
||||
backgroundImage: `linear-gradient(45deg, ${collection.color}30 10%, ${settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
|
||||
} 50%, ${settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
|
||||
} 100%)`,
|
||||
}}
|
||||
className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content"
|
||||
>
|
||||
|
||||
@@ -9,14 +9,14 @@ import Tree, {
|
||||
TreeSourcePosition,
|
||||
TreeDestinationPosition,
|
||||
} from "@atlaskit/tree";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import { Collection } from "@prisma/client";
|
||||
import Link from "next/link";
|
||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||
import { useRouter } from "next/router";
|
||||
import useAccountStore from "@/store/account";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCollections, useUpdateCollection } from "@/hooks/store/collections";
|
||||
import { useUpdateUser, useUser } from "@/hooks/store/user";
|
||||
|
||||
interface ExtendedTreeItem extends TreeItem {
|
||||
data: Collection;
|
||||
@@ -24,53 +24,57 @@ interface ExtendedTreeItem extends TreeItem {
|
||||
|
||||
const CollectionListing = () => {
|
||||
const { t } = useTranslation();
|
||||
const { collections, updateCollection } = useCollectionStore();
|
||||
const { account, updateAccount } = useAccountStore();
|
||||
const updateCollection = useUpdateCollection();
|
||||
const { data: collections = [], isLoading } = useCollections();
|
||||
|
||||
const { data: user = {} } = useUser();
|
||||
const updateUser = useUpdateUser();
|
||||
|
||||
const router = useRouter();
|
||||
const currentPath = router.asPath;
|
||||
|
||||
const [tree, setTree] = useState<TreeData | undefined>();
|
||||
|
||||
const initialTree = useMemo(() => {
|
||||
if (collections.length > 0) {
|
||||
if (
|
||||
// !tree &&
|
||||
collections.length > 0
|
||||
) {
|
||||
return buildTreeFromCollections(
|
||||
collections,
|
||||
router,
|
||||
account.collectionOrder
|
||||
user.collectionOrder
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}, [collections, router]);
|
||||
|
||||
const [tree, setTree] = useState(initialTree);
|
||||
} else return undefined;
|
||||
}, [collections, user, router]);
|
||||
|
||||
useEffect(() => {
|
||||
// if (!tree)
|
||||
setTree(initialTree);
|
||||
}, [initialTree]);
|
||||
|
||||
useEffect(() => {
|
||||
if (account.username) {
|
||||
if (user.username) {
|
||||
if (
|
||||
(!account.collectionOrder || account.collectionOrder.length === 0) &&
|
||||
(!user.collectionOrder || user.collectionOrder.length === 0) &&
|
||||
collections.length > 0
|
||||
)
|
||||
updateAccount({
|
||||
...account,
|
||||
updateUser.mutate({
|
||||
...user,
|
||||
collectionOrder: collections
|
||||
.filter(
|
||||
(e) =>
|
||||
e.parentId === null ||
|
||||
!collections.find((i) => i.id === e.parentId)
|
||||
) // Filter out collections with non-null parentId
|
||||
.map((e) => e.id as number), // Use "as number" to assert that e.id is a number
|
||||
.map((e) => e.id as number),
|
||||
});
|
||||
else {
|
||||
const newCollectionOrder: number[] = [
|
||||
...(account.collectionOrder || []),
|
||||
];
|
||||
const newCollectionOrder: number[] = [...(user.collectionOrder || [])];
|
||||
|
||||
// Start with collections that are in both account.collectionOrder and collections
|
||||
const existingCollectionIds = collections.map((c) => c.id as number);
|
||||
const filteredCollectionOrder = account.collectionOrder.filter((id) =>
|
||||
const filteredCollectionOrder = user.collectionOrder.filter((id: any) =>
|
||||
existingCollectionIds.includes(id)
|
||||
);
|
||||
|
||||
@@ -78,7 +82,7 @@ const CollectionListing = () => {
|
||||
collections.forEach((collection) => {
|
||||
if (
|
||||
!filteredCollectionOrder.includes(collection.id as number) &&
|
||||
(!collection.parentId || collection.ownerId === account.id)
|
||||
(!collection.parentId || collection.ownerId === user.id)
|
||||
) {
|
||||
filteredCollectionOrder.push(collection.id as number);
|
||||
}
|
||||
@@ -87,10 +91,10 @@ const CollectionListing = () => {
|
||||
// check if the newCollectionOrder is the same as the old one
|
||||
if (
|
||||
JSON.stringify(newCollectionOrder) !==
|
||||
JSON.stringify(account.collectionOrder)
|
||||
JSON.stringify(user.collectionOrder)
|
||||
) {
|
||||
updateAccount({
|
||||
...account,
|
||||
updateUser.mutateAsync({
|
||||
...user,
|
||||
collectionOrder: newCollectionOrder,
|
||||
});
|
||||
}
|
||||
@@ -138,9 +142,9 @@ const CollectionListing = () => {
|
||||
);
|
||||
|
||||
if (
|
||||
(movedCollection?.ownerId !== account.id &&
|
||||
(movedCollection?.ownerId !== user.id &&
|
||||
destination.parentId !== source.parentId) ||
|
||||
(destinationCollection?.ownerId !== account.id &&
|
||||
(destinationCollection?.ownerId !== user.id &&
|
||||
destination.parentId !== "root")
|
||||
) {
|
||||
return toast.error(t("cant_change_collection_you_dont_own"));
|
||||
@@ -148,18 +152,25 @@ const CollectionListing = () => {
|
||||
|
||||
setTree((currentTree) => moveItemOnTree(currentTree!, source, destination));
|
||||
|
||||
const updatedCollectionOrder = [...account.collectionOrder];
|
||||
const updatedCollectionOrder = [...user.collectionOrder];
|
||||
|
||||
if (source.parentId !== destination.parentId) {
|
||||
await updateCollection({
|
||||
...movedCollection,
|
||||
parentId:
|
||||
destination.parentId && destination.parentId !== "root"
|
||||
? Number(destination.parentId)
|
||||
: destination.parentId === "root"
|
||||
? "root"
|
||||
: null,
|
||||
} as any);
|
||||
await updateCollection.mutateAsync(
|
||||
{
|
||||
...movedCollection,
|
||||
parentId:
|
||||
destination.parentId && destination.parentId !== "root"
|
||||
? Number(destination.parentId)
|
||||
: destination.parentId === "root"
|
||||
? "root"
|
||||
: null,
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -172,8 +183,8 @@ const CollectionListing = () => {
|
||||
|
||||
updatedCollectionOrder.splice(destination.index, 0, movedCollectionId);
|
||||
|
||||
await updateAccount({
|
||||
...account,
|
||||
await updateUser.mutateAsync({
|
||||
...user,
|
||||
collectionOrder: updatedCollectionOrder,
|
||||
});
|
||||
} else if (
|
||||
@@ -182,8 +193,8 @@ const CollectionListing = () => {
|
||||
) {
|
||||
updatedCollectionOrder.splice(destination.index, 0, movedCollectionId);
|
||||
|
||||
await updateAccount({
|
||||
...account,
|
||||
updateUser.mutate({
|
||||
...user,
|
||||
collectionOrder: updatedCollectionOrder,
|
||||
});
|
||||
} else if (
|
||||
@@ -193,14 +204,22 @@ const CollectionListing = () => {
|
||||
) {
|
||||
updatedCollectionOrder.splice(source.index, 1);
|
||||
|
||||
await updateAccount({
|
||||
...account,
|
||||
await updateUser.mutateAsync({
|
||||
...user,
|
||||
collectionOrder: updatedCollectionOrder,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!tree) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="skeleton h-4 w-full"></div>
|
||||
<div className="skeleton h-4 w-full"></div>
|
||||
<div className="skeleton h-4 w-full"></div>
|
||||
</div>
|
||||
);
|
||||
} else if (!tree) {
|
||||
return (
|
||||
<p className="text-neutral text-xs font-semibold truncate w-full px-2 mt-5 mb-8">
|
||||
{t("you_have_no_collections")}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function FilterSearchDropdown({
|
||||
>
|
||||
<i className="bi-funnel text-neutral text-2xl"></i>
|
||||
</div>
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-56 mt-1">
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box mt-1">
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
@@ -45,7 +45,7 @@ export default function FilterSearchDropdown({
|
||||
setSearchFilter({ ...searchFilter, name: !searchFilter.name })
|
||||
}
|
||||
/>
|
||||
<span className="label-text">{t("name")}</span>
|
||||
<span className="label-text whitespace-nowrap">{t("name")}</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -63,7 +63,7 @@ export default function FilterSearchDropdown({
|
||||
setSearchFilter({ ...searchFilter, url: !searchFilter.url })
|
||||
}
|
||||
/>
|
||||
<span className="label-text">{t("link")}</span>
|
||||
<span className="label-text whitespace-nowrap">{t("link")}</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -84,7 +84,9 @@ export default function FilterSearchDropdown({
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className="label-text">{t("description")}</span>
|
||||
<span className="label-text whitespace-nowrap">
|
||||
{t("description")}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -102,7 +104,7 @@ export default function FilterSearchDropdown({
|
||||
setSearchFilter({ ...searchFilter, tags: !searchFilter.tags })
|
||||
}
|
||||
/>
|
||||
<span className="label-text">{t("tags")}</span>
|
||||
<span className="label-text whitespace-nowrap">{t("tags")}</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -123,8 +125,10 @@ export default function FilterSearchDropdown({
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className="label-text">{t("full_content")}</span>
|
||||
<div className="ml-auto badge badge-sm badge-neutral">
|
||||
<span className="label-text whitespace-nowrap">
|
||||
{t("full_content")}
|
||||
</span>
|
||||
<div className="ml-auto badge badge-sm badge-neutral whitespace-nowrap">
|
||||
{t("slower")}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { styles } from "./styles";
|
||||
import { Options } from "./types";
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
import Select from "react-select";
|
||||
import { useCollections } from "@/hooks/store/collections";
|
||||
|
||||
type Props = {
|
||||
onChange: any;
|
||||
@@ -24,7 +24,8 @@ export default function CollectionSelection({
|
||||
showDefaultValue = true,
|
||||
creatable = true,
|
||||
}: Props) {
|
||||
const { collections } = useCollectionStore();
|
||||
const { data: collections = [] } = useCollections();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [options, setOptions] = useState<Options[]>([]);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import useTagStore from "@/store/tags";
|
||||
import { useEffect, useState } from "react";
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
import { styles } from "./styles";
|
||||
import { Options } from "./types";
|
||||
import { useTags } from "@/hooks/store/tags";
|
||||
|
||||
type Props = {
|
||||
onChange: any;
|
||||
@@ -13,12 +13,12 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function TagSelection({ onChange, defaultValue }: Props) {
|
||||
const { tags } = useTagStore();
|
||||
const { data: tags = [] } = useTags();
|
||||
|
||||
const [options, setOptions] = useState<Options[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const formatedCollections = tags.map((e) => {
|
||||
const formatedCollections = tags.map((e: any) => {
|
||||
return { value: e.id, label: e.name };
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const InstallApp = (props: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
return isOpen && !isPWA() ? (
|
||||
<div className="absolute left-0 right-0 bottom-10 w-full p-5">
|
||||
<div className="fixed left-0 right-0 bottom-10 w-full p-5">
|
||||
<div className="mx-auto w-fit p-2 flex justify-between gap-2 items-center border border-neutral-content rounded-xl bg-base-300 backdrop-blur-md bg-opacity-80 max-w-md">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -5,17 +5,18 @@ import ViewDropdown from "./ViewDropdown";
|
||||
import { TFunction } from "i18next";
|
||||
import BulkDeleteLinksModal from "./ModalContent/BulkDeleteLinksModal";
|
||||
import BulkEditLinksModal from "./ModalContent/BulkEditLinksModal";
|
||||
import toast from "react-hot-toast";
|
||||
import useCollectivePermissions from "@/hooks/useCollectivePermissions";
|
||||
import { useRouter } from "next/router";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { Sort } from "@/types/global";
|
||||
import { Sort, ViewMode } from "@/types/global";
|
||||
import { useBulkDeleteLinks, useLinks } from "@/hooks/store/links";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
t: TFunction<"translation", undefined>;
|
||||
viewMode: string;
|
||||
setViewMode: Dispatch<SetStateAction<string>>;
|
||||
viewMode: ViewMode;
|
||||
setViewMode: Dispatch<SetStateAction<ViewMode>>;
|
||||
searchFilter?: {
|
||||
name: boolean;
|
||||
url: boolean;
|
||||
@@ -48,8 +49,11 @@ const LinkListOptions = ({
|
||||
editMode,
|
||||
setEditMode,
|
||||
}: Props) => {
|
||||
const { links, selectedLinks, setSelectedLinks, deleteLinksById } =
|
||||
useLinkStore();
|
||||
const { selectedLinks, setSelectedLinks } = useLinkStore();
|
||||
|
||||
const deleteLinksById = useBulkDeleteLinks();
|
||||
|
||||
const { links } = useLinks();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -73,23 +77,23 @@ const LinkListOptions = ({
|
||||
};
|
||||
|
||||
const bulkDeleteLinks = async () => {
|
||||
const load = toast.loading(t("deleting_selections"));
|
||||
const load = toast.loading(t("deleting"));
|
||||
|
||||
const response = await deleteLinksById(
|
||||
selectedLinks.map((link) => link.id as number)
|
||||
await deleteLinksById.mutateAsync(
|
||||
selectedLinks.map((link) => link.id as number),
|
||||
{
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
setSelectedLinks([]);
|
||||
toast.success(t("deleted"));
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(
|
||||
selectedLinks.length === 1
|
||||
? t("link_deleted")
|
||||
: t("links_deleted", { count: selectedLinks.length })
|
||||
);
|
||||
} else {
|
||||
toast.error(response.data as string);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -99,57 +103,64 @@ const LinkListOptions = ({
|
||||
|
||||
<div className="flex gap-3 items-center justify-end">
|
||||
<div className="flex gap-2 items-center mt-2">
|
||||
{links.length > 0 && editMode !== undefined && setEditMode && (
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => {
|
||||
setEditMode(!editMode);
|
||||
setSelectedLinks([]);
|
||||
}}
|
||||
className={`btn btn-square btn-sm btn-ghost ${
|
||||
editMode
|
||||
? "bg-primary/20 hover:bg-primary/20"
|
||||
: "hover:bg-neutral/20"
|
||||
}`}
|
||||
>
|
||||
<i className="bi-pencil-fill text-neutral text-xl"></i>
|
||||
</div>
|
||||
)}
|
||||
{links &&
|
||||
links.length > 0 &&
|
||||
editMode !== undefined &&
|
||||
setEditMode && (
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => {
|
||||
setEditMode(!editMode);
|
||||
setSelectedLinks([]);
|
||||
}}
|
||||
className={`btn btn-square btn-sm btn-ghost ${
|
||||
editMode
|
||||
? "bg-primary/20 hover:bg-primary/20"
|
||||
: "hover:bg-neutral/20"
|
||||
}`}
|
||||
>
|
||||
<i className="bi-pencil-fill text-neutral text-xl"></i>
|
||||
</div>
|
||||
)}
|
||||
{searchFilter && setSearchFilter && (
|
||||
<FilterSearchDropdown
|
||||
searchFilter={searchFilter}
|
||||
setSearchFilter={setSearchFilter}
|
||||
/>
|
||||
)}
|
||||
<SortDropdown sortBy={sortBy} setSort={setSortBy} t={t} />
|
||||
<SortDropdown
|
||||
sortBy={sortBy}
|
||||
setSort={(value) => {
|
||||
setSortBy(value);
|
||||
}}
|
||||
t={t}
|
||||
/>
|
||||
<ViewDropdown viewMode={viewMode} setViewMode={setViewMode} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editMode && links.length > 0 && (
|
||||
{links && editMode && links.length > 0 && (
|
||||
<div className="w-full flex justify-between items-center min-h-[32px]">
|
||||
{links.length > 0 && (
|
||||
<div className="flex gap-3 ml-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
onChange={() => handleSelectAll()}
|
||||
checked={
|
||||
selectedLinks.length === links.length && links.length > 0
|
||||
}
|
||||
/>
|
||||
{selectedLinks.length > 0 ? (
|
||||
<span>
|
||||
{selectedLinks.length === 1
|
||||
? t("link_selected")
|
||||
: t("links_selected", { count: selectedLinks.length })}
|
||||
</span>
|
||||
) : (
|
||||
<span>{t("nothing_selected")}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-3 ml-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
onChange={() => handleSelectAll()}
|
||||
checked={
|
||||
selectedLinks.length === links.length && links.length > 0
|
||||
}
|
||||
/>
|
||||
{selectedLinks.length > 0 ? (
|
||||
<span>
|
||||
{selectedLinks.length === 1
|
||||
? t("link_selected")
|
||||
: t("links_selected", { count: selectedLinks.length })}
|
||||
</span>
|
||||
) : (
|
||||
<span>{t("nothing_selected")}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setBulkEditLinksModal(true)}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import LinkCard from "@/components/LinkViews/LinkCard";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import { link } from "fs";
|
||||
import { GridLoader } from "react-spinners";
|
||||
|
||||
export default function CardView({
|
||||
links,
|
||||
editMode,
|
||||
isLoading,
|
||||
}: {
|
||||
links: LinkIncludingShortenedCollectionAndTags[];
|
||||
editMode?: boolean;
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid min-[1901px]:grid-cols-5 min-[1501px]:grid-cols-4 min-[881px]:grid-cols-3 min-[551px]:grid-cols-2 grid-cols-1 gap-5 pb-5">
|
||||
{links.map((e, i) => {
|
||||
return (
|
||||
<LinkCard
|
||||
key={i}
|
||||
link={e}
|
||||
count={i}
|
||||
flipDropdown={i === links.length - 1}
|
||||
editMode={editMode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{isLoading && links.length > 0 && (
|
||||
<GridLoader
|
||||
color="oklch(var(--p))"
|
||||
loading={true}
|
||||
size={20}
|
||||
className="fixed top-5 right-5 opacity-50 z-30"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import LinkList from "@/components/LinkViews/LinkList";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import { GridLoader } from "react-spinners";
|
||||
|
||||
export default function ListView({
|
||||
links,
|
||||
editMode,
|
||||
isLoading,
|
||||
}: {
|
||||
links: LinkIncludingShortenedCollectionAndTags[];
|
||||
editMode?: boolean;
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex gap-1 flex-col">
|
||||
{links.map((e, i) => {
|
||||
return (
|
||||
<LinkList
|
||||
key={i}
|
||||
link={e}
|
||||
count={i}
|
||||
flipDropdown={i === links.length - 1}
|
||||
editMode={editMode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{isLoading && links.length > 0 && (
|
||||
<GridLoader
|
||||
color="oklch(var(--p))"
|
||||
loading={true}
|
||||
size={20}
|
||||
className="fixed top-5 right-5 opacity-50 z-30"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import LinkMasonry from "@/components/LinkViews/LinkMasonry";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import { GridLoader } from "react-spinners";
|
||||
import Masonry from "react-masonry-css";
|
||||
import resolveConfig from "tailwindcss/resolveConfig";
|
||||
import tailwindConfig from "../../../tailwind.config.js";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export default function MasonryView({
|
||||
links,
|
||||
editMode,
|
||||
isLoading,
|
||||
}: {
|
||||
links: LinkIncludingShortenedCollectionAndTags[];
|
||||
editMode?: boolean;
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const fullConfig = resolveConfig(tailwindConfig as any);
|
||||
|
||||
const breakpointColumnsObj = useMemo(() => {
|
||||
return {
|
||||
default: 5,
|
||||
1900: 4,
|
||||
1500: 3,
|
||||
880: 2,
|
||||
550: 1,
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Masonry
|
||||
breakpointCols={breakpointColumnsObj}
|
||||
columnClassName="flex flex-col gap-5 !w-full"
|
||||
className="grid min-[1901px]:grid-cols-5 min-[1501px]:grid-cols-4 min-[881px]:grid-cols-3 min-[551px]:grid-cols-2 grid-cols-1 gap-5 pb-5"
|
||||
>
|
||||
{links.map((e, i) => {
|
||||
return (
|
||||
<LinkMasonry
|
||||
key={i}
|
||||
link={e}
|
||||
count={i}
|
||||
flipDropdown={i === links.length - 1}
|
||||
editMode={editMode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{isLoading && links.length > 0 && (
|
||||
<GridLoader
|
||||
color="oklch(var(--p))"
|
||||
loading={true}
|
||||
size={20}
|
||||
className="fixed top-5 right-5 opacity-50 z-30"
|
||||
/>
|
||||
)}
|
||||
</Masonry>
|
||||
);
|
||||
}
|
||||
@@ -7,11 +7,11 @@ import usePermissions from "@/hooks/usePermissions";
|
||||
import EditLinkModal from "@/components/ModalContent/EditLinkModal";
|
||||
import DeleteLinkModal from "@/components/ModalContent/DeleteLinkModal";
|
||||
import PreservedFormatsModal from "@/components/ModalContent/PreservedFormatsModal";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { toast } from "react-hot-toast";
|
||||
import useAccountStore from "@/store/account";
|
||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
import { useDeleteLink, useUpdateLink } from "@/hooks/store/links";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
@@ -39,41 +39,35 @@ export default function LinkActions({
|
||||
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
|
||||
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
|
||||
|
||||
const { account } = useAccountStore();
|
||||
const { data: user = {} } = useUser();
|
||||
|
||||
const { removeLink, updateLink } = useLinkStore();
|
||||
const updateLink = useUpdateLink();
|
||||
const deleteLink = useDeleteLink();
|
||||
|
||||
const pinLink = async () => {
|
||||
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0];
|
||||
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0] ? true : false;
|
||||
|
||||
const load = toast.loading(t("applying"));
|
||||
const load = toast.loading(t("updating"));
|
||||
|
||||
const response = await updateLink({
|
||||
...link,
|
||||
pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }],
|
||||
});
|
||||
await updateLink.mutateAsync(
|
||||
{
|
||||
...link,
|
||||
pinnedBy: isAlreadyPinned ? undefined : [{ id: user.id }],
|
||||
},
|
||||
{
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(isAlreadyPinned ? t("link_unpinned") : t("link_unpinned"));
|
||||
} else {
|
||||
toast.error(response.data as string);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteLink = async () => {
|
||||
const load = toast.loading(t("deleting"));
|
||||
|
||||
const response = await removeLink(link.id as number);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("deleted"));
|
||||
} else {
|
||||
toast.error(response.data as string);
|
||||
}
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.success(
|
||||
isAlreadyPinned ? t("link_unpinned") : t("link_pinned")
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -92,7 +86,7 @@ export default function LinkActions({
|
||||
<i title="More" className="bi-three-dots text-xl" />
|
||||
</div>
|
||||
<ul
|
||||
className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mr-1 ${
|
||||
className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box mr-1 ${
|
||||
alignToTop ? "" : "translate-y-10"
|
||||
}`}
|
||||
>
|
||||
@@ -104,6 +98,7 @@ export default function LinkActions({
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
pinLink();
|
||||
}}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{link?.pinnedBy && link.pinnedBy[0]
|
||||
? t("unpin")
|
||||
@@ -119,6 +114,7 @@ export default function LinkActions({
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
toggleShowInfo();
|
||||
}}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{!linkInfo ? t("show_link_details") : t("hide_link_details")}
|
||||
</div>
|
||||
@@ -133,6 +129,7 @@ export default function LinkActions({
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setEditLinkModal(true);
|
||||
}}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("edit_link")}
|
||||
</div>
|
||||
@@ -147,6 +144,7 @@ export default function LinkActions({
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setPreservedFormatsModal(true);
|
||||
}}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("preserved_formats")}
|
||||
</div>
|
||||
@@ -157,10 +155,27 @@ export default function LinkActions({
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
onClick={async (e) => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
e.shiftKey ? deleteLink() : setDeleteLinkModal(true);
|
||||
e.shiftKey
|
||||
? async () => {
|
||||
const load = toast.loading(t("deleting"));
|
||||
|
||||
await deleteLink.mutateAsync(link.id as number, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.success(t("deleted"));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
: setDeleteLinkModal(true);
|
||||
}}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("delete")}
|
||||
</div>
|
||||
@@ -184,7 +199,7 @@ export default function LinkActions({
|
||||
{preservedFormatsModal && (
|
||||
<PreservedFormatsModal
|
||||
onClose={() => setPreservedFormatsModal(false)}
|
||||
activeLink={link}
|
||||
link={link}
|
||||
/>
|
||||
)}
|
||||
{/* {expandedLink ? (
|
||||
|
||||
+16
-10
@@ -5,7 +5,6 @@ import {
|
||||
} from "@/types/global";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions";
|
||||
import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate";
|
||||
@@ -13,14 +12,16 @@ import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection
|
||||
import Image from "next/image";
|
||||
import { previewAvailable } from "@/lib/shared/getArchiveValidity";
|
||||
import Link from "next/link";
|
||||
import LinkIcon from "./LinkComponents/LinkIcon";
|
||||
import LinkIcon from "./LinkIcon";
|
||||
import useOnScreen from "@/hooks/useOnScreen";
|
||||
import { generateLinkHref } from "@/lib/client/generateLinkHref";
|
||||
import useAccountStore from "@/store/account";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import toast from "react-hot-toast";
|
||||
import LinkTypeBadge from "./LinkComponents/LinkTypeBadge";
|
||||
import LinkTypeBadge from "./LinkTypeBadge";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCollections } from "@/hooks/store/collections";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
import { useGetLink, useLinks } from "@/hooks/store/links";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
@@ -33,11 +34,16 @@ type Props = {
|
||||
export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const viewMode = localStorage.getItem("viewMode") || "card";
|
||||
const { collections } = useCollectionStore();
|
||||
const { account } = useAccountStore();
|
||||
const { data: collections = [] } = useCollections();
|
||||
|
||||
const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore();
|
||||
const { data: user = {} } = useUser();
|
||||
|
||||
const { setSelectedLinks, selectedLinks } = useLinkStore();
|
||||
|
||||
const {
|
||||
data: { data: links = [] },
|
||||
} = useLinks();
|
||||
const getLink = useGetLink();
|
||||
|
||||
useEffect(() => {
|
||||
if (!editMode) {
|
||||
@@ -93,7 +99,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
||||
link.preview !== "unavailable"
|
||||
) {
|
||||
interval = setInterval(async () => {
|
||||
getLink(link.id as number);
|
||||
getLink.mutateAsync(link.id as number);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
@@ -131,7 +137,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
||||
<div
|
||||
className="rounded-2xl cursor-pointer h-full flex flex-col justify-between"
|
||||
onClick={() =>
|
||||
!editMode && window.open(generateLinkHref(link, account), "_blank")
|
||||
!editMode && window.open(generateLinkHref(link, user), "_blank")
|
||||
}
|
||||
>
|
||||
<div>
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
} from "@/types/global";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
export default function LinkCollection({
|
||||
@@ -13,22 +12,22 @@ export default function LinkCollection({
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
collection: CollectionIncludingMembersAndLinkCount;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/collections/${link.collection.id}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100 select-none"
|
||||
title={collection?.name}
|
||||
>
|
||||
<i
|
||||
className="bi-folder-fill text-lg drop-shadow"
|
||||
style={{ color: collection?.color }}
|
||||
></i>
|
||||
<p className="truncate capitalize">{collection?.name}</p>
|
||||
</Link>
|
||||
<>
|
||||
<Link
|
||||
href={`/collections/${link.collection.id}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100 select-none"
|
||||
title={collection?.name}
|
||||
>
|
||||
<i
|
||||
className="bi-folder-fill text-lg drop-shadow"
|
||||
style={{ color: collection?.color }}
|
||||
></i>
|
||||
<p className="truncate capitalize">{collection?.name}</p>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+11
-7
@@ -4,7 +4,6 @@ import {
|
||||
} from "@/types/global";
|
||||
import { useEffect, useState } from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions";
|
||||
import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate";
|
||||
@@ -12,11 +11,13 @@ import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection
|
||||
import LinkIcon from "@/components/LinkViews/LinkComponents/LinkIcon";
|
||||
import { isPWA } from "@/lib/client/utils";
|
||||
import { generateLinkHref } from "@/lib/client/generateLinkHref";
|
||||
import useAccountStore from "@/store/account";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import toast from "react-hot-toast";
|
||||
import LinkTypeBadge from "./LinkComponents/LinkTypeBadge";
|
||||
import LinkTypeBadge from "./LinkTypeBadge";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCollections } from "@/hooks/store/collections";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
import { useLinks } from "@/hooks/store/links";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
@@ -33,9 +34,12 @@ export default function LinkCardCompact({
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { collections } = useCollectionStore();
|
||||
const { account } = useAccountStore();
|
||||
const { links, setSelectedLinks, selectedLinks } = useLinkStore();
|
||||
const { data: collections = [] } = useCollections();
|
||||
|
||||
const { data: user = {} } = useUser();
|
||||
const { setSelectedLinks, selectedLinks } = useLinkStore();
|
||||
|
||||
const { links } = useLinks();
|
||||
|
||||
useEffect(() => {
|
||||
if (!editMode) {
|
||||
@@ -119,7 +123,7 @@ export default function LinkCardCompact({
|
||||
<div
|
||||
className="flex items-center cursor-pointer w-full"
|
||||
onClick={() =>
|
||||
!editMode && window.open(generateLinkHref(link, account), "_blank")
|
||||
!editMode && window.open(generateLinkHref(link, user), "_blank")
|
||||
}
|
||||
>
|
||||
<div className="shrink-0">
|
||||
+13
-9
@@ -5,7 +5,6 @@ import {
|
||||
} from "@/types/global";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions";
|
||||
import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate";
|
||||
@@ -13,14 +12,16 @@ import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection
|
||||
import Image from "next/image";
|
||||
import { previewAvailable } from "@/lib/shared/getArchiveValidity";
|
||||
import Link from "next/link";
|
||||
import LinkIcon from "./LinkComponents/LinkIcon";
|
||||
import LinkIcon from "./LinkIcon";
|
||||
import useOnScreen from "@/hooks/useOnScreen";
|
||||
import { generateLinkHref } from "@/lib/client/generateLinkHref";
|
||||
import useAccountStore from "@/store/account";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import toast from "react-hot-toast";
|
||||
import LinkTypeBadge from "./LinkComponents/LinkTypeBadge";
|
||||
import LinkTypeBadge from "./LinkTypeBadge";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCollections } from "@/hooks/store/collections";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
import { useGetLink, useLinks } from "@/hooks/store/links";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
@@ -33,10 +34,13 @@ type Props = {
|
||||
export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { collections } = useCollectionStore();
|
||||
const { account } = useAccountStore();
|
||||
const { data: collections = [] } = useCollections();
|
||||
const { data: user = {} } = useUser();
|
||||
|
||||
const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore();
|
||||
const { setSelectedLinks, selectedLinks } = useLinkStore();
|
||||
|
||||
const { links } = useLinks();
|
||||
const getLink = useGetLink();
|
||||
|
||||
useEffect(() => {
|
||||
if (!editMode) {
|
||||
@@ -92,7 +96,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
|
||||
link.preview !== "unavailable"
|
||||
) {
|
||||
interval = setInterval(async () => {
|
||||
getLink(link.id as number);
|
||||
getLink.mutateAsync(link.id as number);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
@@ -130,7 +134,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
|
||||
<div
|
||||
className="rounded-2xl cursor-pointer"
|
||||
onClick={() =>
|
||||
!editMode && window.open(generateLinkHref(link, account), "_blank")
|
||||
!editMode && window.open(generateLinkHref(link, user), "_blank")
|
||||
}
|
||||
>
|
||||
<div className="relative rounded-t-2xl overflow-hidden">
|
||||
@@ -0,0 +1,238 @@
|
||||
import LinkCard from "@/components/LinkViews/LinkComponents/LinkCard";
|
||||
import {
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
ViewMode,
|
||||
} from "@/types/global";
|
||||
import { useEffect } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import LinkMasonry from "@/components/LinkViews/LinkComponents/LinkMasonry";
|
||||
import Masonry from "react-masonry-css";
|
||||
import resolveConfig from "tailwindcss/resolveConfig";
|
||||
import tailwindConfig from "../../tailwind.config.js";
|
||||
import { useMemo } from "react";
|
||||
import LinkList from "@/components/LinkViews/LinkComponents/LinkList";
|
||||
|
||||
export function CardView({
|
||||
links,
|
||||
editMode,
|
||||
isLoading,
|
||||
placeholders,
|
||||
hasNextPage,
|
||||
placeHolderRef,
|
||||
}: {
|
||||
links?: LinkIncludingShortenedCollectionAndTags[];
|
||||
editMode?: boolean;
|
||||
isLoading?: boolean;
|
||||
placeholders?: number[];
|
||||
hasNextPage?: boolean;
|
||||
placeHolderRef?: any;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid min-[1901px]:grid-cols-5 min-[1501px]:grid-cols-4 min-[881px]:grid-cols-3 min-[551px]:grid-cols-2 grid-cols-1 gap-5 pb-5">
|
||||
{links?.map((e, i) => {
|
||||
return (
|
||||
<LinkCard
|
||||
key={i}
|
||||
link={e}
|
||||
count={i}
|
||||
flipDropdown={i === links.length - 1}
|
||||
editMode={editMode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{(hasNextPage || isLoading) &&
|
||||
placeholders?.map((e, i) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-4"
|
||||
ref={e === 1 ? placeHolderRef : undefined}
|
||||
key={i}
|
||||
>
|
||||
<div className="skeleton h-40 w-full"></div>
|
||||
<div className="skeleton h-3 w-2/3"></div>
|
||||
<div className="skeleton h-3 w-full"></div>
|
||||
<div className="skeleton h-3 w-full"></div>
|
||||
<div className="skeleton h-3 w-1/3"></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MasonryView({
|
||||
links,
|
||||
editMode,
|
||||
isLoading,
|
||||
placeholders,
|
||||
hasNextPage,
|
||||
placeHolderRef,
|
||||
}: {
|
||||
links?: LinkIncludingShortenedCollectionAndTags[];
|
||||
editMode?: boolean;
|
||||
isLoading?: boolean;
|
||||
placeholders?: number[];
|
||||
hasNextPage?: boolean;
|
||||
placeHolderRef?: any;
|
||||
}) {
|
||||
const fullConfig = resolveConfig(tailwindConfig as any);
|
||||
|
||||
const breakpointColumnsObj = useMemo(() => {
|
||||
return {
|
||||
default: 5,
|
||||
1900: 4,
|
||||
1500: 3,
|
||||
880: 2,
|
||||
550: 1,
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Masonry
|
||||
breakpointCols={breakpointColumnsObj}
|
||||
columnClassName="flex flex-col gap-5 !w-full"
|
||||
className="grid min-[1901px]:grid-cols-5 min-[1501px]:grid-cols-4 min-[881px]:grid-cols-3 min-[551px]:grid-cols-2 grid-cols-1 gap-5 pb-5"
|
||||
>
|
||||
{links?.map((e, i) => {
|
||||
return (
|
||||
<LinkMasonry
|
||||
key={i}
|
||||
link={e}
|
||||
count={i}
|
||||
flipDropdown={i === links.length - 1}
|
||||
editMode={editMode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{(hasNextPage || isLoading) &&
|
||||
placeholders?.map((e, i) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-4"
|
||||
ref={e === 1 ? placeHolderRef : undefined}
|
||||
key={i}
|
||||
>
|
||||
<div className="skeleton h-40 w-full"></div>
|
||||
<div className="skeleton h-3 w-2/3"></div>
|
||||
<div className="skeleton h-3 w-full"></div>
|
||||
<div className="skeleton h-3 w-full"></div>
|
||||
<div className="skeleton h-3 w-1/3"></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Masonry>
|
||||
);
|
||||
}
|
||||
|
||||
export function ListView({
|
||||
links,
|
||||
editMode,
|
||||
isLoading,
|
||||
placeholders,
|
||||
hasNextPage,
|
||||
placeHolderRef,
|
||||
}: {
|
||||
links?: LinkIncludingShortenedCollectionAndTags[];
|
||||
editMode?: boolean;
|
||||
isLoading?: boolean;
|
||||
placeholders?: number[];
|
||||
hasNextPage?: boolean;
|
||||
placeHolderRef?: any;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex gap-1 flex-col">
|
||||
{links?.map((e, i) => {
|
||||
return (
|
||||
<LinkList
|
||||
key={i}
|
||||
link={e}
|
||||
count={i}
|
||||
flipDropdown={i === links.length - 1}
|
||||
editMode={editMode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{(hasNextPage || isLoading) &&
|
||||
placeholders?.map((e, i) => {
|
||||
return (
|
||||
<div
|
||||
ref={e === 1 ? placeHolderRef : undefined}
|
||||
key={i}
|
||||
className="flex gap-4 p-4"
|
||||
>
|
||||
<div className="skeleton h-16 w-16"></div>
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<div className="skeleton h-3 w-2/3"></div>
|
||||
<div className="skeleton h-3 w-full"></div>
|
||||
<div className="skeleton h-3 w-1/3"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Links({
|
||||
layout,
|
||||
links,
|
||||
editMode,
|
||||
placeholderCount,
|
||||
useData,
|
||||
}: {
|
||||
layout: ViewMode;
|
||||
links?: LinkIncludingShortenedCollectionAndTags[];
|
||||
editMode?: boolean;
|
||||
placeholderCount?: number;
|
||||
useData?: any;
|
||||
}) {
|
||||
const { ref, inView } = useInView();
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && useData?.fetchNextPage && useData?.hasNextPage) {
|
||||
useData.fetchNextPage();
|
||||
}
|
||||
}, [useData, inView]);
|
||||
|
||||
if (layout === ViewMode.List) {
|
||||
return (
|
||||
<ListView
|
||||
links={links}
|
||||
editMode={editMode}
|
||||
isLoading={useData?.isLoading}
|
||||
placeholders={placeholderCountToArray(placeholderCount)}
|
||||
hasNextPage={useData?.hasNextPage}
|
||||
placeHolderRef={ref}
|
||||
/>
|
||||
);
|
||||
} else if (layout === ViewMode.Masonry) {
|
||||
return (
|
||||
<MasonryView
|
||||
links={links}
|
||||
editMode={editMode}
|
||||
isLoading={useData?.isLoading}
|
||||
placeholders={placeholderCountToArray(placeholderCount)}
|
||||
hasNextPage={useData?.hasNextPage}
|
||||
placeHolderRef={ref}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Default to card view
|
||||
return (
|
||||
<CardView
|
||||
links={links}
|
||||
editMode={editMode}
|
||||
isLoading={useData?.isLoading}
|
||||
placeholders={placeholderCountToArray(placeholderCount)}
|
||||
hasNextPage={useData?.hasNextPage}
|
||||
placeHolderRef={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const placeholderCountToArray = (num?: number) =>
|
||||
num ? Array.from({ length: num }, (_, i) => i + 1) : [];
|
||||
@@ -41,7 +41,7 @@ export default function MobileNavigation({}: Props) {
|
||||
<i className="bi-plus text-5xl pointer-events-none"></i>
|
||||
</span>
|
||||
</div>
|
||||
<ul className="dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box w-40 mb-1 -ml-12">
|
||||
<ul className="dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box mb-1 -ml-12">
|
||||
<li>
|
||||
<div
|
||||
onClick={() => {
|
||||
@@ -50,6 +50,7 @@ export default function MobileNavigation({}: Props) {
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("new_link")}
|
||||
</div>
|
||||
@@ -62,6 +63,7 @@ export default function MobileNavigation({}: Props) {
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("upload_file")}
|
||||
</div>
|
||||
@@ -74,6 +76,7 @@ export default function MobileNavigation({}: Props) {
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("new_collection")}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import Button from "../ui/Button";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useBulkDeleteLinks } from "@/hooks/store/links";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -11,22 +12,29 @@ type Props = {
|
||||
|
||||
export default function BulkDeleteLinksModal({ onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { selectedLinks, setSelectedLinks, deleteLinksById } = useLinkStore();
|
||||
const { selectedLinks, setSelectedLinks } = useLinkStore();
|
||||
|
||||
const deleteLinksById = useBulkDeleteLinks();
|
||||
|
||||
const deleteLink = async () => {
|
||||
const load = toast.loading(t("deleting"));
|
||||
|
||||
const response = await deleteLinksById(
|
||||
selectedLinks.map((link) => link.id as number)
|
||||
await deleteLinksById.mutateAsync(
|
||||
selectedLinks.map((link) => link.id as number),
|
||||
{
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
setSelectedLinks([]);
|
||||
onClose();
|
||||
toast.success(t("deleted"));
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("deleted"));
|
||||
setSelectedLinks([]);
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useBulkEditLinks } from "@/hooks/store/links";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -13,13 +14,14 @@ type Props = {
|
||||
|
||||
export default function BulkEditLinksModal({ onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { updateLinks, selectedLinks, setSelectedLinks } = useLinkStore();
|
||||
const { selectedLinks, setSelectedLinks } = useLinkStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const [removePreviousTags, setRemovePreviousTags] = useState(false);
|
||||
const [updatedValues, setUpdatedValues] = useState<
|
||||
Pick<LinkIncludingShortenedCollectionAndTags, "tags" | "collectionId">
|
||||
>({ tags: [] });
|
||||
|
||||
const updateLinks = useBulkEditLinks();
|
||||
const setCollection = (e: any) => {
|
||||
const collectionId = e?.value || null;
|
||||
setUpdatedValues((prevValues) => ({ ...prevValues, collectionId }));
|
||||
@@ -36,22 +38,28 @@ export default function BulkEditLinksModal({ onClose }: Props) {
|
||||
|
||||
const load = toast.loading(t("updating"));
|
||||
|
||||
const response = await updateLinks(
|
||||
selectedLinks,
|
||||
removePreviousTags,
|
||||
updatedValues
|
||||
await updateLinks.mutateAsync(
|
||||
{
|
||||
links: selectedLinks,
|
||||
newData: updatedValues,
|
||||
removePreviousTags,
|
||||
},
|
||||
{
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
setSelectedLinks([]);
|
||||
onClose();
|
||||
toast.success(t("updated"));
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("updated"));
|
||||
setSelectedLinks([]);
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
|
||||
setSubmitLoader(false);
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import toast from "react-hot-toast";
|
||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||
import { useRouter } from "next/router";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import Modal from "../Modal";
|
||||
import Button from "../ui/Button";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useDeleteCollection } from "@/hooks/store/collections";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -22,7 +22,6 @@ export default function DeleteCollectionModal({
|
||||
const [collection, setCollection] =
|
||||
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const { removeCollection } = useCollectionStore();
|
||||
const router = useRouter();
|
||||
const [inputField, setInputField] = useState("");
|
||||
const permissions = usePermissions(collection.id as number);
|
||||
@@ -31,6 +30,8 @@ export default function DeleteCollectionModal({
|
||||
setCollection(activeCollection);
|
||||
}, []);
|
||||
|
||||
const deleteCollection = useDeleteCollection();
|
||||
|
||||
const submit = async () => {
|
||||
if (permissions === true && collection.name !== inputField) return;
|
||||
if (!submitLoader) {
|
||||
@@ -41,17 +42,19 @@ export default function DeleteCollectionModal({
|
||||
|
||||
const load = toast.loading(t("deleting_collection"));
|
||||
|
||||
let response = await removeCollection(collection.id as number);
|
||||
deleteCollection.mutateAsync(collection.id as number, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("deleted"));
|
||||
onClose();
|
||||
router.push("/collections");
|
||||
} else {
|
||||
toast.error(response.data as string);
|
||||
}
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
onClose();
|
||||
toast.success(t("deleted"));
|
||||
router.push("/collections");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
setSubmitLoader(false);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import { useRouter } from "next/router";
|
||||
import Button from "../ui/Button";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useDeleteLink } from "@/hooks/store/links";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -16,31 +16,32 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||
const { removeLink } = useLinkStore();
|
||||
|
||||
const deleteLink = useDeleteLink();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
setLink(activeLink);
|
||||
}, []);
|
||||
|
||||
const deleteLink = async () => {
|
||||
const submit = async () => {
|
||||
const load = toast.loading(t("deleting"));
|
||||
|
||||
const response = await removeLink(link.id as number);
|
||||
await deleteLink.mutateAsync(link.id as number, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("deleted"));
|
||||
} else {
|
||||
toast.error(response.data as string);
|
||||
}
|
||||
|
||||
if (router.pathname.startsWith("/links/[id]")) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
|
||||
onClose();
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
if (router.pathname.startsWith("/links/[id]")) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
toast.success(t("deleted"));
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -61,7 +62,7 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
|
||||
|
||||
<p>{t("shift_key_tip")}</p>
|
||||
|
||||
<Button className="ml-auto" intent="destructive" onClick={deleteLink}>
|
||||
<Button className="ml-auto" intent="destructive" onClick={submit}>
|
||||
<i className="bi-trash text-xl" />
|
||||
{t("delete")}
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import useUserStore from "@/store/admin/users";
|
||||
import Button from "../ui/Button";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useDeleteUser } from "@/hooks/store/admin/users";
|
||||
import { useState } from "react";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -11,22 +11,22 @@ type Props = {
|
||||
|
||||
export default function DeleteUserModal({ onClose, userId }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { removeUser } = useUserStore();
|
||||
|
||||
const deleteUser = async () => {
|
||||
const load = toast.loading(t("deleting_user"));
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const deleteUser = useDeleteUser();
|
||||
|
||||
const response = await removeUser(userId);
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
|
||||
toast.dismiss(load);
|
||||
await deleteUser.mutateAsync(userId, {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("user_deleted"));
|
||||
} else {
|
||||
toast.error(response.data as string);
|
||||
setSubmitLoader(false);
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -45,7 +45,7 @@ export default function DeleteUserModal({ onClose, userId }: Props) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button className="ml-auto" intent="destructive" onClick={deleteUser}>
|
||||
<Button className="ml-auto" intent="destructive" onClick={submit}>
|
||||
<i className="bi-trash text-xl" />
|
||||
{t("delete_confirmation")}
|
||||
</Button>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import toast from "react-hot-toast";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||
import Modal from "../Modal";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useUpdateCollection } from "@/hooks/store/collections";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -21,7 +21,7 @@ export default function EditCollectionModal({
|
||||
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
||||
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const { updateCollection } = useCollectionStore();
|
||||
const updateCollection = useUpdateCollection();
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
@@ -32,14 +32,18 @@ export default function EditCollectionModal({
|
||||
|
||||
const load = toast.loading(t("updating_collection"));
|
||||
|
||||
let response = await updateCollection(collection as any);
|
||||
await updateCollection.mutateAsync(collection, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("updated"));
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
onClose();
|
||||
toast.success(t("updated"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
setSubmitLoader(false);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import toast from "react-hot-toast";
|
||||
import {
|
||||
AccountSettings,
|
||||
@@ -8,13 +7,14 @@ import {
|
||||
Member,
|
||||
} from "@/types/global";
|
||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||
import useAccountStore from "@/store/account";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import ProfilePhoto from "../ProfilePhoto";
|
||||
import addMemberToCollection from "@/lib/client/addMemberToCollection";
|
||||
import Modal from "../Modal";
|
||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useUpdateCollection } from "@/hooks/store/collections";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -31,7 +31,7 @@ export default function EditCollectionSharingModal({
|
||||
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
||||
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const { updateCollection } = useCollectionStore();
|
||||
const updateCollection = useUpdateCollection();
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
@@ -40,24 +40,26 @@ export default function EditCollectionSharingModal({
|
||||
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading(t("updating"));
|
||||
const load = toast.loading(t("updating_collection"));
|
||||
|
||||
let response;
|
||||
await updateCollection.mutateAsync(collection, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
response = await updateCollection(collection as any);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("updated"));
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
onClose();
|
||||
toast.success(t("updated"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
setSubmitLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const { account } = useAccountStore();
|
||||
const { data: user = {} } = useUser();
|
||||
const permissions = usePermissions(collection.id as number);
|
||||
|
||||
const currentURL = new URL(document.URL);
|
||||
@@ -163,7 +165,7 @@ export default function EditCollectionSharingModal({
|
||||
onKeyDown={(e) =>
|
||||
e.key === "Enter" &&
|
||||
addMemberToCollection(
|
||||
account.username as string,
|
||||
user.username as string,
|
||||
memberUsername || "",
|
||||
collection,
|
||||
setMemberState,
|
||||
@@ -175,7 +177,7 @@ export default function EditCollectionSharingModal({
|
||||
<div
|
||||
onClick={() =>
|
||||
addMemberToCollection(
|
||||
account.username as string,
|
||||
user.username as string,
|
||||
memberUsername || "",
|
||||
collection,
|
||||
setMemberState,
|
||||
@@ -273,7 +275,7 @@ export default function EditCollectionSharingModal({
|
||||
{roleLabel}
|
||||
<i className="bi-chevron-down"></i>
|
||||
</div>
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-xl w-64 mt-1">
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-xl mt-1">
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
@@ -312,10 +314,12 @@ export default function EditCollectionSharingModal({
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-bold">
|
||||
<p className="font-bold whitespace-nowrap">
|
||||
{t("viewer")}
|
||||
</p>
|
||||
<p>{t("viewer_desc")}</p>
|
||||
<p className="whitespace-nowrap">
|
||||
{t("viewer_desc")}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
@@ -357,10 +361,12 @@ export default function EditCollectionSharingModal({
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-bold">
|
||||
<p className="font-bold whitespace-nowrap">
|
||||
{t("contributor")}
|
||||
</p>
|
||||
<p>{t("contributor_desc")}</p>
|
||||
<p className="whitespace-nowrap">
|
||||
{t("contributor_desc")}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
@@ -402,10 +408,12 @@ export default function EditCollectionSharingModal({
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-bold">
|
||||
<p className="font-bold whitespace-nowrap">
|
||||
{t("admin")}
|
||||
</p>
|
||||
<p>{t("admin_desc")}</p>
|
||||
<p className="whitespace-nowrap">
|
||||
{t("admin_desc")}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
|
||||
@@ -3,12 +3,12 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import toast from "react-hot-toast";
|
||||
import Link from "next/link";
|
||||
import Modal from "../Modal";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useUpdateLink } from "@/hooks/store/links";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -27,9 +27,10 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
const { updateLink } = useLinkStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const updateLink = useUpdateLink();
|
||||
|
||||
const setCollection = (e: any) => {
|
||||
if (e?.__isNew__) e.value = null;
|
||||
setLink({
|
||||
@@ -50,19 +51,23 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
const load = toast.loading(t("updating"));
|
||||
let response = await updateLink(link);
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("updated"));
|
||||
onClose();
|
||||
} else {
|
||||
toast.error(response.data as string);
|
||||
}
|
||||
const load = toast.loading(t("updating"));
|
||||
|
||||
await updateLink.mutateAsync(link, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
onClose();
|
||||
toast.success(t("updated"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
setSubmitLoader(false);
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import toast from "react-hot-toast";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { Collection } from "@prisma/client";
|
||||
import Modal from "../Modal";
|
||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||
import useAccountStore from "@/store/account";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCreateCollection } from "@/hooks/store/collections";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -25,15 +23,14 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
|
||||
} as Partial<Collection>;
|
||||
|
||||
const [collection, setCollection] = useState<Partial<Collection>>(initial);
|
||||
const { setAccount } = useAccountStore();
|
||||
const { data } = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
setCollection(initial);
|
||||
}, []);
|
||||
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const { addCollection } = useCollectionStore();
|
||||
|
||||
const createCollection = useCreateCollection();
|
||||
|
||||
const submit = async () => {
|
||||
if (submitLoader) return;
|
||||
@@ -43,16 +40,18 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
|
||||
|
||||
const load = toast.loading(t("creating"));
|
||||
|
||||
let response = await addCollection(collection as any);
|
||||
toast.dismiss(load);
|
||||
await createCollection.mutateAsync(collection, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("created_success"));
|
||||
if (response.data) {
|
||||
setAccount(data?.user.id as number);
|
||||
onClose();
|
||||
}
|
||||
} else toast.error(response.data as string);
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
onClose();
|
||||
toast.success(t("created"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
setSubmitLoader(false);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCollections } from "@/hooks/store/collections";
|
||||
import { useAddLink } from "@/hooks/store/links";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -40,11 +39,13 @@ export default function NewLinkModal({ onClose }: Props) {
|
||||
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(initial);
|
||||
const { addLink } = useLinkStore();
|
||||
|
||||
const addLink = useAddLink();
|
||||
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const [optionsExpanded, setOptionsExpanded] = useState(false);
|
||||
const router = useRouter();
|
||||
const { collections } = useCollectionStore();
|
||||
const { data: collections = [] } = useCollections();
|
||||
|
||||
const setCollection = (e: any) => {
|
||||
if (e?.__isNew__) e.value = null;
|
||||
@@ -87,15 +88,22 @@ export default function NewLinkModal({ onClose }: Props) {
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading(t("creating_link"));
|
||||
const response = await addLink(link);
|
||||
toast.dismiss(load);
|
||||
if (response.ok) {
|
||||
toast.success(t("link_created"));
|
||||
onClose();
|
||||
} else {
|
||||
toast.error(response.data as string);
|
||||
}
|
||||
|
||||
await addLink.mutateAsync(link, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
onClose();
|
||||
toast.success(t("link_created"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
setSubmitLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@ import TextInput from "@/components/TextInput";
|
||||
import { TokenExpiry } from "@/types/global";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import useTokenStore from "@/store/tokens";
|
||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||
import Button from "../ui/Button";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useAddToken } from "@/hooks/store/tokens";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -15,7 +15,7 @@ type Props = {
|
||||
export default function NewTokenModal({ onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [newToken, setNewToken] = useState("");
|
||||
const { addToken } = useTokenStore();
|
||||
const addToken = useAddToken();
|
||||
|
||||
const initial = {
|
||||
name: "",
|
||||
@@ -28,16 +28,20 @@ export default function NewTokenModal({ onClose }: Props) {
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading(t("creating_token"));
|
||||
|
||||
const { ok, data } = await addToken(token);
|
||||
await addToken.mutateAsync(token, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (ok) {
|
||||
toast.success(t("token_created"));
|
||||
setNewToken((data as any).secretKey);
|
||||
} else toast.error(data as string);
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
setNewToken(data.secretKey);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
setSubmitLoader(false);
|
||||
}
|
||||
@@ -111,7 +115,7 @@ export default function NewTokenModal({ onClose }: Props) {
|
||||
>
|
||||
{getLabel(token.expires)}
|
||||
</Button>
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-xl w-full sm:w-52 mt-1">
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-xl mt-1">
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
@@ -131,7 +135,9 @@ export default function NewTokenModal({ onClose }: Props) {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">{t("7_days")}</span>
|
||||
<span className="label-text whitespace-nowrap">
|
||||
{t("7_days")}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -150,7 +156,9 @@ export default function NewTokenModal({ onClose }: Props) {
|
||||
setToken({ ...token, expires: TokenExpiry.oneMonth });
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">{t("30_days")}</span>
|
||||
<span className="label-text whitespace-nowrap">
|
||||
{t("30_days")}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -172,7 +180,9 @@ export default function NewTokenModal({ onClose }: Props) {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">{t("60_days")}</span>
|
||||
<span className="label-text whitespace-nowrap">
|
||||
{t("60_days")}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -194,7 +204,9 @@ export default function NewTokenModal({ onClose }: Props) {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">{t("90_days")}</span>
|
||||
<span className="label-text whitespace-nowrap">
|
||||
{t("90_days")}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -213,7 +225,9 @@ export default function NewTokenModal({ onClose }: Props) {
|
||||
setToken({ ...token, expires: TokenExpiry.never });
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">{t("no_expiration")}</span>
|
||||
<span className="label-text whitespace-nowrap">
|
||||
{t("no_expiration")}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import useUserStore from "@/store/admin/users";
|
||||
import TextInput from "../TextInput";
|
||||
import { FormEvent, useState } from "react";
|
||||
import { useTranslation, Trans } from "next-i18next";
|
||||
import { useAddUser } from "@/hooks/store/admin/users";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -20,7 +20,9 @@ const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true";
|
||||
|
||||
export default function NewUserModal({ onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { addUser } = useUserStore();
|
||||
|
||||
const addUser = useAddUser();
|
||||
|
||||
const [form, setForm] = useState<FormData>({
|
||||
name: "",
|
||||
username: "",
|
||||
@@ -44,24 +46,15 @@ export default function NewUserModal({ onClose }: Props) {
|
||||
};
|
||||
|
||||
if (checkFields()) {
|
||||
if (form.password.length < 8)
|
||||
return toast.error(t("password_length_error"));
|
||||
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading(t("creating_account"));
|
||||
await addUser.mutateAsync(form, {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const response = await addUser(form);
|
||||
|
||||
toast.dismiss(load);
|
||||
setSubmitLoader(false);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("user_created"));
|
||||
onClose();
|
||||
} else {
|
||||
toast.error(response.data as string);
|
||||
}
|
||||
} else {
|
||||
toast.error(t("fill_all_fields_error"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import {
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
ArchivedFormat,
|
||||
@@ -17,23 +16,22 @@ import {
|
||||
screenshotAvailable,
|
||||
} from "@/lib/shared/getArchiveValidity";
|
||||
import PreservedFormatRow from "@/components/PreserverdFormatRow";
|
||||
import useAccountStore from "@/store/account";
|
||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { BeatLoader } from "react-spinners";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
import { useGetLink } from "@/hooks/store/links";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
};
|
||||
|
||||
export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||
export default function PreservedFormatsModal({ onClose, link }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const session = useSession();
|
||||
const { getLink } = useLinkStore();
|
||||
const { account } = useAccountStore();
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||
const getLink = useGetLink();
|
||||
const { data: user = {} } = useUser();
|
||||
const router = useRouter();
|
||||
|
||||
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
|
||||
@@ -44,20 +42,20 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOwner = async () => {
|
||||
if (link.collection.ownerId !== account.id) {
|
||||
if (link.collection.ownerId !== user.id) {
|
||||
const owner = await getPublicUserData(
|
||||
link.collection.ownerId as number
|
||||
);
|
||||
setCollectionOwner(owner);
|
||||
} else if (link.collection.ownerId === account.id) {
|
||||
} else if (link.collection.ownerId === user.id) {
|
||||
setCollectionOwner({
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
username: account.username,
|
||||
image: account.image,
|
||||
archiveAsScreenshot: account.archiveAsScreenshot,
|
||||
archiveAsMonolith: account.archiveAsScreenshot,
|
||||
archiveAsPDF: account.archiveAsPDF,
|
||||
id: user.id as number,
|
||||
name: user.name,
|
||||
username: user.username as string,
|
||||
image: user.image as string,
|
||||
archiveAsScreenshot: user.archiveAsScreenshot as boolean,
|
||||
archiveAsMonolith: user.archiveAsScreenshot as boolean,
|
||||
archiveAsPDF: user.archiveAsPDF as boolean,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -93,20 +91,14 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await getLink(link.id as number, isPublic);
|
||||
setLink(
|
||||
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
||||
);
|
||||
await getLink.mutateAsync(link.id as number);
|
||||
})();
|
||||
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
|
||||
if (!isReady()) {
|
||||
interval = setInterval(async () => {
|
||||
const data = await getLink(link.id as number, isPublic);
|
||||
setLink(
|
||||
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
||||
);
|
||||
await getLink.mutateAsync(link.id as number);
|
||||
}, 5000);
|
||||
} else {
|
||||
if (interval) {
|
||||
@@ -132,10 +124,8 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
const newLink = await getLink(link?.id as number);
|
||||
setLink(
|
||||
(newLink as any).response as LinkIncludingShortenedCollectionAndTags
|
||||
);
|
||||
await getLink.mutateAsync(link?.id as number);
|
||||
|
||||
toast.success(t("link_being_archived"));
|
||||
} else toast.error(data.response);
|
||||
};
|
||||
@@ -145,9 +135,9 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||
<p className="text-xl font-thin">{t("preserved_formats")}</p>
|
||||
<div className="divider mb-2 mt-1"></div>
|
||||
{screenshotAvailable(link) ||
|
||||
pdfAvailable(link) ||
|
||||
readabilityAvailable(link) ||
|
||||
monolithAvailable(link) ? (
|
||||
pdfAvailable(link) ||
|
||||
readabilityAvailable(link) ||
|
||||
monolithAvailable(link) ? (
|
||||
<p className="mb-3">{t("available_formats")}</p>
|
||||
) : (
|
||||
""
|
||||
@@ -159,7 +149,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||
name={t("webpage")}
|
||||
icon={"bi-filetype-html"}
|
||||
format={ArchivedFormat.monolith}
|
||||
activeLink={link}
|
||||
link={link}
|
||||
downloadable={true}
|
||||
/>
|
||||
)}
|
||||
@@ -173,7 +163,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||
? ArchivedFormat.png
|
||||
: ArchivedFormat.jpeg
|
||||
}
|
||||
activeLink={link}
|
||||
link={link}
|
||||
downloadable={true}
|
||||
/>
|
||||
)}
|
||||
@@ -183,7 +173,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||
name={t("pdf")}
|
||||
icon={"bi-file-earmark-pdf"}
|
||||
format={ArchivedFormat.pdf}
|
||||
activeLink={link}
|
||||
link={link}
|
||||
downloadable={true}
|
||||
/>
|
||||
)}
|
||||
@@ -193,7 +183,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||
name={t("readable")}
|
||||
icon={"bi-file-earmark-text"}
|
||||
format={ArchivedFormat.readability}
|
||||
activeLink={link}
|
||||
link={link}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -224,9 +214,8 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`flex flex-col sm:flex-row gap-3 items-center justify-center ${
|
||||
isReady() ? "sm:mt " : ""
|
||||
}`}
|
||||
className={`flex flex-col sm:flex-row gap-3 items-center justify-center ${isReady() ? "sm:mt " : ""
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
href={`https://web.archive.org/web/${link?.url?.replace(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import useTokenStore from "@/store/tokens";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import Button from "../ui/Button";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { AccessToken } from "@prisma/client";
|
||||
import { useRevokeToken } from "@/hooks/store/tokens";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -15,7 +15,7 @@ export default function DeleteTokenModal({ onClose, activeToken }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [token, setToken] = useState<AccessToken>(activeToken);
|
||||
|
||||
const { revokeToken } = useTokenStore();
|
||||
const revokeToken = useRevokeToken();
|
||||
|
||||
useEffect(() => {
|
||||
setToken(activeToken);
|
||||
@@ -24,17 +24,18 @@ export default function DeleteTokenModal({ onClose, activeToken }: Props) {
|
||||
const deleteLink = async () => {
|
||||
const load = toast.loading(t("deleting"));
|
||||
|
||||
const response = await revokeToken(token.id as number);
|
||||
await revokeToken.mutateAsync(token.id, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("token_revoked"));
|
||||
} else {
|
||||
toast.error(response.data as string);
|
||||
}
|
||||
|
||||
onClose();
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
onClose();
|
||||
toast.success(t("token_revoked"));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,8 +3,6 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import useLinkStore from "@/store/links";
|
||||
import {
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
ArchivedFormat,
|
||||
@@ -14,6 +12,8 @@ import { useRouter } from "next/router";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCollections } from "@/hooks/store/collections";
|
||||
import { useUploadFile } from "@/hooks/store/links";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -45,11 +45,11 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(initial);
|
||||
const [file, setFile] = useState<File>();
|
||||
|
||||
const { uploadFile } = useLinkStore();
|
||||
const uploadFile = useUploadFile();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const [optionsExpanded, setOptionsExpanded] = useState(false);
|
||||
const router = useRouter();
|
||||
const { collections } = useCollectionStore();
|
||||
const { data: collections = [] } = useCollections();
|
||||
|
||||
const setCollection = (e: any) => {
|
||||
if (e?.__isNew__) e.value = null;
|
||||
@@ -115,20 +115,26 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||
// }
|
||||
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading(t("creating"));
|
||||
|
||||
const response = await uploadFile(link, file);
|
||||
await uploadFile.mutateAsync(
|
||||
{ link, file },
|
||||
{
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
toast.dismiss(load);
|
||||
if (response.ok) {
|
||||
toast.success(t("created_success"));
|
||||
onClose();
|
||||
} else {
|
||||
toast.error(response.data as string);
|
||||
}
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
onClose();
|
||||
toast.success(t("created_success"));
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setSubmitLoader(false);
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function Navbar() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box w-40 mt-1">
|
||||
<ul className="dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box mt-1">
|
||||
<li>
|
||||
<div
|
||||
onClick={() => {
|
||||
@@ -75,6 +75,7 @@ export default function Navbar() {
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("new_link")}
|
||||
</div>
|
||||
@@ -87,6 +88,7 @@ export default function Navbar() {
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("upload_file")}
|
||||
</div>
|
||||
@@ -99,6 +101,7 @@ export default function Navbar() {
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("new_collection")}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import {
|
||||
ArchivedFormat,
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
@@ -11,7 +9,7 @@ type Props = {
|
||||
name: string;
|
||||
icon: string;
|
||||
format: ArchivedFormat;
|
||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
downloadable?: boolean;
|
||||
};
|
||||
|
||||
@@ -19,43 +17,13 @@ export default function PreservedFormatRow({
|
||||
name,
|
||||
icon,
|
||||
format,
|
||||
activeLink,
|
||||
link,
|
||||
downloadable,
|
||||
}: Props) {
|
||||
const { getLink } = useLinkStore();
|
||||
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { data } = await getLink(link.id as number, isPublic);
|
||||
setLink(data as LinkIncludingShortenedCollectionAndTags);
|
||||
})();
|
||||
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
if (link?.image === "pending" || link?.pdf === "pending") {
|
||||
interval = setInterval(async () => {
|
||||
const { data } = await getLink(link.id as number, isPublic);
|
||||
setLink(data as LinkIncludingShortenedCollectionAndTags);
|
||||
}, 5000);
|
||||
} else {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
}, [link?.image, link?.pdf, link?.readable, link?.monolith]);
|
||||
|
||||
const handleDownload = () => {
|
||||
const path = `/api/v1/archives/${link?.id}?format=${format}`;
|
||||
fetch(path)
|
||||
@@ -90,20 +58,18 @@ export default function PreservedFormatRow({
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
{downloadable ||
|
||||
(false && (
|
||||
<div
|
||||
onClick={() => handleDownload()}
|
||||
className="btn btn-sm btn-square"
|
||||
>
|
||||
<i className="bi-cloud-arrow-down text-xl text-neutral" />
|
||||
</div>
|
||||
))}
|
||||
{downloadable || false ? (
|
||||
<div
|
||||
onClick={() => handleDownload()}
|
||||
className="btn btn-sm btn-square"
|
||||
>
|
||||
<i className="bi-cloud-arrow-down text-xl text-neutral" />
|
||||
</div>
|
||||
) : undefined}
|
||||
|
||||
<Link
|
||||
href={`${
|
||||
isPublic ? "/public" : ""
|
||||
}/preserved/${link?.id}?format=${format}`}
|
||||
href={`${isPublic ? "/public" : ""
|
||||
}/preserved/${link?.id}?format=${format}`}
|
||||
target="_blank"
|
||||
className="btn btn-sm btn-square"
|
||||
>
|
||||
@@ -112,4 +78,4 @@ export default function PreservedFormatRow({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||
import ProfilePhoto from "./ProfilePhoto";
|
||||
import useAccountStore from "@/store/account";
|
||||
import Link from "next/link";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
|
||||
export default function ProfileDropdown() {
|
||||
const { t } = useTranslation();
|
||||
const { settings, updateSettings } = useLocalSettingsStore();
|
||||
const { account } = useAccountStore();
|
||||
const { data: user = {} } = useUser();
|
||||
|
||||
const isAdmin = account.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1);
|
||||
const isAdmin = user.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1);
|
||||
|
||||
const handleToggle = () => {
|
||||
const newTheme = settings.theme === "dark" ? "light" : "dark";
|
||||
@@ -27,14 +27,12 @@ export default function ProfileDropdown() {
|
||||
className="btn btn-circle btn-ghost"
|
||||
>
|
||||
<ProfilePhoto
|
||||
src={account.image ? account.image : undefined}
|
||||
src={user.image ? user.image : undefined}
|
||||
priority={true}
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
className={`dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box ${
|
||||
isAdmin ? "w-48" : "w-40"
|
||||
} mt-1`}
|
||||
className={`dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box mt-1`}
|
||||
>
|
||||
<li>
|
||||
<Link
|
||||
@@ -42,6 +40,7 @@ export default function ProfileDropdown() {
|
||||
onClick={() => (document?.activeElement as HTMLElement)?.blur()}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("settings")}
|
||||
</Link>
|
||||
@@ -54,6 +53,7 @@ export default function ProfileDropdown() {
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("switch_to", {
|
||||
theme: settings.theme === "light" ? t("dark") : t("light"),
|
||||
@@ -67,6 +67,7 @@ export default function ProfileDropdown() {
|
||||
onClick={() => (document?.activeElement as HTMLElement)?.blur()}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("server_administration")}
|
||||
</Link>
|
||||
@@ -80,6 +81,7 @@ export default function ProfileDropdown() {
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("logout")}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import { readabilityAvailable } from "@/lib/shared/getArchiveValidity";
|
||||
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||
import useLinkStore from "@/store/links";
|
||||
import {
|
||||
ArchivedFormat,
|
||||
CollectionIncludingMembersAndLinkCount,
|
||||
@@ -14,8 +13,9 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import LinkActions from "./LinkViews/LinkComponents/LinkActions";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCollections } from "@/hooks/store/collections";
|
||||
import { useGetLink } from "@/hooks/store/links";
|
||||
|
||||
type LinkContent = {
|
||||
title: string;
|
||||
@@ -45,8 +45,8 @@ export default function ReadableView({ link }: Props) {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { getLink } = useLinkStore();
|
||||
const { collections } = useCollectionStore();
|
||||
const getLink = useGetLink();
|
||||
const { data: collections = [] } = useCollections();
|
||||
|
||||
const collection = useMemo(() => {
|
||||
return collections.find(
|
||||
@@ -73,7 +73,7 @@ export default function ReadableView({ link }: Props) {
|
||||
}, [link]);
|
||||
|
||||
useEffect(() => {
|
||||
if (link) getLink(link?.id as number);
|
||||
if (link) getLink.mutateAsync(link?.id as number);
|
||||
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
if (
|
||||
@@ -87,7 +87,10 @@ export default function ReadableView({ link }: Props) {
|
||||
!link?.readable ||
|
||||
!link?.monolith)
|
||||
) {
|
||||
interval = setInterval(() => getLink(link.id as number), 5000);
|
||||
interval = setInterval(
|
||||
() => getLink.mutateAsync(link.id as number),
|
||||
5000
|
||||
);
|
||||
} else {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
|
||||
+14
-7
@@ -1,5 +1,3 @@
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import useTagStore from "@/store/tags";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -7,6 +5,8 @@ import { Disclosure, Transition } from "@headlessui/react";
|
||||
import SidebarHighlightLink from "@/components/SidebarHighlightLink";
|
||||
import CollectionListing from "@/components/CollectionListing";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCollections } from "@/hooks/store/collections";
|
||||
import { useTags } from "@/hooks/store/tags";
|
||||
|
||||
export default function Sidebar({ className }: { className?: string }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -22,8 +22,9 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||
}
|
||||
);
|
||||
|
||||
const { collections } = useCollectionStore();
|
||||
const { tags } = useTagStore();
|
||||
const { data: collections } = useCollections();
|
||||
|
||||
const { data: tags = [], isLoading } = useTags();
|
||||
const [active, setActive] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
@@ -127,10 +128,16 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||
leaveTo="transform opacity-0 -translate-y-3"
|
||||
>
|
||||
<Disclosure.Panel className="flex flex-col gap-1">
|
||||
{tags[0] ? (
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="skeleton h-4 w-full"></div>
|
||||
<div className="skeleton h-4 w-full"></div>
|
||||
<div className="skeleton h-4 w-full"></div>
|
||||
</div>
|
||||
) : tags[0] ? (
|
||||
tags
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((e, i) => {
|
||||
.sort((a: any, b: any) => a.name.localeCompare(b.name))
|
||||
.map((e: any, i: any) => {
|
||||
return (
|
||||
<Link key={i} href={`/tags/${e.id}`}>
|
||||
<div
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function SidebarHighlightLink({
|
||||
return (
|
||||
<Link href={href}>
|
||||
<div
|
||||
title={title}
|
||||
className={`${
|
||||
active || false
|
||||
? "bg-primary/20"
|
||||
@@ -28,7 +29,7 @@ export default function SidebarHighlightLink({
|
||||
<i className={`${icon} text-primary text-2xl drop-shadow`}></i>
|
||||
</div>
|
||||
<div className={"mt-1"}>
|
||||
<p className="truncate w-full font-semibold text-sm">{title}</p>
|
||||
<p className="truncate w-full font-semibold text-xs">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { Dispatch, SetStateAction } from "react";
|
||||
import React, { Dispatch, SetStateAction, useEffect } from "react";
|
||||
import { Sort } from "@/types/global";
|
||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||
import { TFunction } from "i18next";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
|
||||
type Props = {
|
||||
sortBy: Sort;
|
||||
@@ -10,6 +11,12 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function SortDropdown({ sortBy, setSort, t }: Props) {
|
||||
const { updateSettings } = useLocalSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
updateSettings({ sortBy });
|
||||
}, [sortBy]);
|
||||
|
||||
return (
|
||||
<div className="dropdown dropdown-bottom dropdown-end">
|
||||
<div
|
||||
@@ -20,7 +27,7 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) {
|
||||
>
|
||||
<i className="bi-chevron-expand text-neutral text-2xl"></i>
|
||||
</div>
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-xl w-52 mt-1">
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-xl mt-1">
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
@@ -34,7 +41,9 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) {
|
||||
checked={sortBy === Sort.DateNewestFirst}
|
||||
onChange={() => setSort(Sort.DateNewestFirst)}
|
||||
/>
|
||||
<span className="label-text">{t("date_newest_first")}</span>
|
||||
<span className="label-text whitespace-nowrap">
|
||||
{t("date_newest_first")}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -50,7 +59,9 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) {
|
||||
checked={sortBy === Sort.DateOldestFirst}
|
||||
onChange={() => setSort(Sort.DateOldestFirst)}
|
||||
/>
|
||||
<span className="label-text">{t("date_oldest_first")}</span>
|
||||
<span className="label-text whitespace-nowrap">
|
||||
{t("date_oldest_first")}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -66,7 +77,7 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) {
|
||||
checked={sortBy === Sort.NameAZ}
|
||||
onChange={() => setSort(Sort.NameAZ)}
|
||||
/>
|
||||
<span className="label-text">{t("name_az")}</span>
|
||||
<span className="label-text whitespace-nowrap">{t("name_az")}</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -82,7 +93,7 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) {
|
||||
checked={sortBy === Sort.NameZA}
|
||||
onChange={() => setSort(Sort.NameZA)}
|
||||
/>
|
||||
<span className="label-text">{t("name_za")}</span>
|
||||
<span className="label-text whitespace-nowrap">{t("name_za")}</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -98,7 +109,9 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) {
|
||||
checked={sortBy === Sort.DescriptionAZ}
|
||||
onChange={() => setSort(Sort.DescriptionAZ)}
|
||||
/>
|
||||
<span className="label-text">{t("description_az")}</span>
|
||||
<span className="label-text whitespace-nowrap">
|
||||
{t("description_az")}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -114,7 +127,9 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) {
|
||||
checked={sortBy === Sort.DescriptionZA}
|
||||
onChange={() => setSort(Sort.DescriptionZA)}
|
||||
/>
|
||||
<span className="label-text">{t("description_za")}</span>
|
||||
<span className="label-text whitespace-nowrap">
|
||||
{t("description_za")}
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -4,8 +4,8 @@ import useLocalSettingsStore from "@/store/localSettings";
|
||||
import { ViewMode } from "@/types/global";
|
||||
|
||||
type Props = {
|
||||
viewMode: string;
|
||||
setViewMode: Dispatch<SetStateAction<string>>;
|
||||
viewMode: ViewMode;
|
||||
setViewMode: Dispatch<SetStateAction<ViewMode>>;
|
||||
};
|
||||
|
||||
export default function ViewDropdown({ viewMode, setViewMode }: Props) {
|
||||
@@ -19,7 +19,7 @@ export default function ViewDropdown({ viewMode, setViewMode }: Props) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateSettings({ viewMode: viewMode as ViewMode });
|
||||
updateSettings({ viewMode });
|
||||
}, [viewMode]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
color: string;
|
||||
size: string;
|
||||
};
|
||||
|
||||
const Loader = (props: Props) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
width={props.size}
|
||||
height={props.size}
|
||||
className={props.className}
|
||||
style={{
|
||||
shapeRendering: "auto",
|
||||
display: "block",
|
||||
background: "rgba(255, 255, 255, 0)",
|
||||
}}
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<g>
|
||||
<g transform="rotate(0 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="-0.9166666666666666s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(30 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="-0.8333333333333334s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(60 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="-0.75s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(90 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="-0.6666666666666666s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(120 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="-0.5833333333333334s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(150 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="-0.5s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(180 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="-0.4166666666666667s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(210 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="-0.3333333333333333s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(240 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="-0.25s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(270 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="-0.16666666666666666s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(300 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="-0.08333333333333333s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(330 50 50)">
|
||||
<rect
|
||||
fill={props.color}
|
||||
height="12"
|
||||
width="6"
|
||||
ry="1.8"
|
||||
rx="1.8"
|
||||
y="24"
|
||||
x="47"
|
||||
>
|
||||
<animate
|
||||
repeatCount="indefinite"
|
||||
begin="0s"
|
||||
dur="1s"
|
||||
keyTimes="0;1"
|
||||
values="1;0"
|
||||
attributeName="opacity"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<g></g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
Reference in New Issue
Block a user