Accepted incoming changes

This commit is contained in:
daniel31x13
2024-11-12 10:09:02 -05:00
55 changed files with 6714 additions and 1281 deletions

View File

@@ -35,6 +35,7 @@ READABILITY_MAX_BUFFER=
PREVIEW_MAX_BUFFER=
IMPORT_LIMIT=
PLAYWRIGHT_LAUNCH_OPTIONS_EXECUTABLE_PATH=
MAX_WORKERS=
# AWS S3 Settings
SPACES_KEY=

View File

@@ -27,7 +27,7 @@ const CollectionListing = () => {
const updateCollection = useUpdateCollection();
const { data: collections = [], isLoading } = useCollections();
const { data: user = {} } = useUser();
const { data: user = {}, refetch } = useUser();
const updateUser = useUpdateUser();
const router = useRouter();
@@ -36,10 +36,7 @@ const CollectionListing = () => {
const [tree, setTree] = useState<TreeData | undefined>();
const initialTree = useMemo(() => {
if (
// !tree &&
collections.length > 0
) {
if (collections.length > 0) {
return buildTreeFromCollections(
collections,
router,
@@ -49,12 +46,12 @@ const CollectionListing = () => {
}, [collections, user, router]);
useEffect(() => {
// if (!tree)
setTree(initialTree);
}, [initialTree]);
useEffect(() => {
if (user.username) {
refetch();
if (
(!user.collectionOrder || user.collectionOrder.length === 0) &&
collections.length > 0
@@ -62,11 +59,7 @@ const CollectionListing = () => {
updateUser.mutate({
...user,
collectionOrder: collections
.filter(
(e) =>
e.parentId === null ||
!collections.find((i) => i.id === e.parentId)
) // Filter out collections with non-null parentId
.filter((e) => e.parentId === null)
.map((e) => e.id as number),
});
else {
@@ -100,7 +93,7 @@ const CollectionListing = () => {
}
}
}
}, [collections]);
}, [user, collections]);
const onExpand = (movedCollectionId: ItemId) => {
setTree((currentTree) =>

View File

@@ -8,13 +8,15 @@ export default function dashboardItem({
icon: string;
}) {
return (
<div className="flex items-center">
<div className="w-[4rem] aspect-square flex justify-center items-center bg-primary/20 rounded-xl select-none">
<div className="flex items-center justify-between w-full rounded-2xl border border-neutral-content p-3 bg-gradient-to-tr from-neutral-content/70 to-50% to-base-200">
<div className="w-14 aspect-square flex justify-center items-center bg-primary/20 rounded-xl select-none">
<i className={`${icon} text-primary text-3xl drop-shadow`}></i>
</div>
<div className="ml-4 flex flex-col justify-center">
<p className="text-neutral text-xs tracking-wider">{name}</p>
<p className="font-thin text-5xl text-primary mt-0.5">{value}</p>
<p className="text-neutral text-xs tracking-wider text-right">{name}</p>
<p className="font-thin text-4xl text-primary mt-0.5 text-right">
{value || 0}
</p>
</div>
</div>
);

81
components/Drawer.tsx Normal file
View File

@@ -0,0 +1,81 @@
import React, { ReactNode, useEffect } from "react";
import { Drawer as D } from "vaul";
import clsx from "clsx";
type Props = {
toggleDrawer: Function;
children: ReactNode;
className?: string;
dismissible?: boolean;
};
export default function Drawer({
toggleDrawer,
className,
children,
dismissible = true,
}: Props) {
const [drawerIsOpen, setDrawerIsOpen] = React.useState(true);
useEffect(() => {
if (window.innerWidth >= 640) {
document.body.style.overflow = "hidden";
document.body.style.position = "relative";
return () => {
document.body.style.overflow = "auto";
document.body.style.position = "";
};
}
}, []);
if (window.innerWidth < 640) {
return (
<D.Root
open={drawerIsOpen}
onClose={() => dismissible && setDrawerIsOpen(false)}
onAnimationEnd={(isOpen) => !isOpen && toggleDrawer()}
dismissible={dismissible}
>
<D.Portal>
<D.Overlay className="fixed inset-0 bg-black/40" />
<D.Content className="flex flex-col rounded-t-2xl mt-24 fixed bottom-0 left-0 right-0 z-30 h-[90%] !select-auto focus:outline-none">
<div
className={clsx(
"p-4 bg-base-100 rounded-t-2xl flex-1 border-neutral-content border-t overflow-y-auto",
className
)}
data-testid="mobile-modal-container"
>
<div data-testid="mobile-modal-slider" />
{children}
</div>
</D.Content>
</D.Portal>
</D.Root>
);
} else {
return (
<D.Root
open={drawerIsOpen}
onClose={() => dismissible && setDrawerIsOpen(false)}
onAnimationEnd={(isOpen) => !isOpen && toggleDrawer()}
dismissible={dismissible}
direction="right"
>
<D.Portal>
<D.Overlay className="fixed inset-0 bg-black/10 z-20" />
<D.Content className="bg-white flex flex-col h-full w-2/5 min-w-[30rem] mt-24 fixed bottom-0 right-0 z-40 !select-auto focus:outline-none">
<div
className={clsx(
"p-4 bg-base-100 flex-1 border-neutral-content border-l overflow-y-auto",
className
)}
>
{children}
</div>
</D.Content>
</D.Portal>
</D.Root>
);
}
}

91
components/IconGrid.tsx Normal file
View File

@@ -0,0 +1,91 @@
import { icons } from "@/lib/client/icons";
import Fuse from "fuse.js";
import { forwardRef, useMemo } from "react";
import { FixedSizeGrid as Grid } from "react-window";
const fuse = new Fuse(icons, {
keys: [{ name: "name", weight: 4 }, "tags", "categories"],
threshold: 0.2,
useExtendedSearch: true,
});
type Props = {
query: string;
color: string;
weight: "light" | "regular" | "bold" | "fill" | "duotone" | "thin";
iconName?: string;
setIconName: Function;
};
const IconGrid = ({ query, color, weight, iconName, setIconName }: Props) => {
const filteredIcons = useMemo(() => {
if (!query) {
return icons;
}
return fuse.search(query).map((result) => result.item);
}, [query]);
const columnCount = 6;
const rowCount = Math.ceil(filteredIcons.length / columnCount);
const GUTTER_SIZE = 5;
const Cell = ({ columnIndex, rowIndex, style }: any) => {
const index = rowIndex * columnCount + columnIndex;
if (index >= filteredIcons.length) return null; // Prevent overflow
const icon = filteredIcons[index];
const IconComponent = icon.Icon;
return (
<div
style={{
...style,
left: style.left + GUTTER_SIZE,
top: style.top + GUTTER_SIZE,
width: style.width - GUTTER_SIZE,
height: style.height - GUTTER_SIZE,
}}
onClick={() => setIconName(icon.pascal_name)}
className={`cursor-pointer p-[6px] rounded-lg bg-base-100 w-full ${
icon.pascal_name === iconName
? "outline outline-1 outline-primary"
: ""
}`}
>
<IconComponent size={32} weight={weight} color={color} />
</div>
);
};
const InnerElementType = forwardRef(({ style, ...rest }: any, ref) => (
<div
ref={ref}
style={{
...style,
paddingLeft: GUTTER_SIZE,
paddingTop: GUTTER_SIZE,
}}
{...rest}
/>
));
InnerElementType.displayName = "InnerElementType";
return (
<Grid
columnCount={columnCount}
rowCount={rowCount}
columnWidth={50}
rowHeight={50}
innerElementType={InnerElementType}
width={320}
height={158}
itemData={filteredIcons}
className="hide-scrollbar ml-[4px] w-fit"
>
{Cell}
</Grid>
);
};
export default IconGrid;

147
components/IconPopover.tsx Normal file
View File

@@ -0,0 +1,147 @@
import React, { useState } from "react";
import TextInput from "./TextInput";
import Popover from "./Popover";
import { HexColorPicker } from "react-colorful";
import { useTranslation } from "next-i18next";
import IconGrid from "./IconGrid";
import clsx from "clsx";
type Props = {
alignment?: string;
color: string;
setColor: Function;
iconName?: string;
setIconName: Function;
weight: "light" | "regular" | "bold" | "fill" | "duotone" | "thin";
setWeight: Function;
reset: Function;
className?: string;
onClose: Function;
};
const IconPopover = ({
alignment,
color,
setColor,
iconName,
setIconName,
weight,
setWeight,
reset,
className,
onClose,
}: Props) => {
const { t } = useTranslation();
const [query, setQuery] = useState("");
return (
<Popover
onClose={() => onClose()}
className={clsx(
className,
"fade-in bg-base-200 border border-neutral-content p-3 w-[22.5rem] rounded-lg shadow-md"
)}
>
<div className="flex flex-col gap-3 w-full h-full">
<TextInput
className="p-2 rounded w-full h-7 text-sm"
placeholder={t("search")}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<div className="grid grid-cols-6 gap-1 w-full overflow-y-auto h-44 border border-neutral-content bg-base-100 rounded-md p-2">
<IconGrid
query={query}
color={color}
weight={weight}
iconName={iconName}
setIconName={setIconName}
/>
</div>
<div className="flex gap-3 color-picker w-full justify-between">
<HexColorPicker
color={color}
onChange={(e) => setColor(e)}
className="border border-neutral-content rounded-lg"
/>
<div className="grid grid-cols-2 gap-3 text-sm">
<label className="flex items-center cursor-pointer">
<input
type="radio"
className="radio radio-primary mr-2"
value="regular"
checked={weight === "regular"}
onChange={() => setWeight("regular")}
/>
{t("regular")}
</label>
<label className="flex items-center cursor-pointer">
<input
type="radio"
className="radio radio-primary mr-2"
value="thin"
checked={weight === "thin"}
onChange={() => setWeight("thin")}
/>
{t("thin")}
</label>
<label className="flex items-center cursor-pointer">
<input
type="radio"
className="radio radio-primary mr-2"
value="light"
checked={weight === "light"}
onChange={() => setWeight("light")}
/>
{t("light_icon")}
</label>
<label className="flex items-center cursor-pointer">
<input
type="radio"
className="radio radio-primary mr-2"
value="bold"
checked={weight === "bold"}
onChange={() => setWeight("bold")}
/>
{t("bold")}
</label>
<label className="flex items-center cursor-pointer">
<input
type="radio"
className="radio radio-primary mr-2"
value="fill"
checked={weight === "fill"}
onChange={() => setWeight("fill")}
/>
{t("fill")}
</label>
<label className="flex items-center cursor-pointer">
<input
type="radio"
className="radio radio-primary mr-2"
value="duotone"
checked={weight === "duotone"}
onChange={() => setWeight("duotone")}
/>
{t("duotone")}
</label>
</div>
</div>
<div className="flex flex-row gap-2 justify-between items-center mt-2">
<div
className="btn btn-ghost btn-xs w-fit"
onClick={reset as React.MouseEventHandler<HTMLDivElement>}
>
{t("reset_defaults")}
</div>
<p className="text-neutral text-xs">{t("click_out_to_apply")}</p>
</div>
</div>
</Popover>
);
};
export default IconPopover;

View File

@@ -16,6 +16,10 @@ export const styles: StylesConfig = {
},
transition: "all 50ms",
}),
menu: (styles) => ({
...styles,
zIndex: 10,
}),
control: (styles, state) => ({
...styles,
fontFamily: font,

687
components/LinkDetails.tsx Normal file
View File

@@ -0,0 +1,687 @@
import React, { useEffect, useState } from "react";
import {
LinkIncludingShortenedCollectionAndTags,
ArchivedFormat,
} from "@/types/global";
import Link from "next/link";
import {
pdfAvailable,
readabilityAvailable,
monolithAvailable,
screenshotAvailable,
previewAvailable,
} from "@/lib/shared/getArchiveValidity";
import PreservedFormatRow from "@/components/PreserverdFormatRow";
import getPublicUserData from "@/lib/client/getPublicUserData";
import { useTranslation } from "next-i18next";
import { BeatLoader } from "react-spinners";
import { useUser } from "@/hooks/store/user";
import {
useGetLink,
useUpdateLink,
useUpdatePreview,
} from "@/hooks/store/links";
import LinkIcon from "./LinkViews/LinkComponents/LinkIcon";
import CopyButton from "./CopyButton";
import { useRouter } from "next/router";
import Icon from "./Icon";
import { IconWeight } from "@phosphor-icons/react";
import Image from "next/image";
import clsx from "clsx";
import toast from "react-hot-toast";
import CollectionSelection from "./InputSelect/CollectionSelection";
import TagSelection from "./InputSelect/TagSelection";
import unescapeString from "@/lib/client/unescapeString";
import IconPopover from "./IconPopover";
import TextInput from "./TextInput";
import usePermissions from "@/hooks/usePermissions";
type Props = {
className?: string;
activeLink: LinkIncludingShortenedCollectionAndTags;
standalone?: boolean;
mode?: "view" | "edit";
setMode?: Function;
onUpdateArchive?: Function;
};
export default function LinkDetails({
className,
activeLink,
standalone,
mode = "view",
setMode,
onUpdateArchive,
}: Props) {
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
useEffect(() => {
setLink(activeLink);
}, [activeLink]);
const permissions = usePermissions(link.collection.id as number);
const { t } = useTranslation();
const getLink = useGetLink();
const { data: user = {} } = useUser();
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
useEffect(() => {
const fetchOwner = async () => {
if (link.collection.ownerId !== user.id) {
const owner = await getPublicUserData(
link.collection.ownerId as number
);
setCollectionOwner(owner);
} else if (link.collection.ownerId === user.id) {
setCollectionOwner({
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,
});
}
};
fetchOwner();
}, [link.collection.ownerId]);
const isReady = () => {
return (
link &&
(collectionOwner.archiveAsScreenshot === true
? link.pdf && link.pdf !== "pending"
: true) &&
(collectionOwner.archiveAsMonolith === true
? link.monolith && link.monolith !== "pending"
: true) &&
(collectionOwner.archiveAsPDF === true
? link.pdf && link.pdf !== "pending"
: true) &&
link.readable &&
link.readable !== "pending"
);
};
const atLeastOneFormatAvailable = () => {
return (
screenshotAvailable(link) ||
pdfAvailable(link) ||
readabilityAvailable(link) ||
monolithAvailable(link)
);
};
useEffect(() => {
(async () => {
await getLink.mutateAsync({
id: link.id as number,
});
})();
let interval: any;
if (!isReady()) {
interval = setInterval(async () => {
await getLink.mutateAsync({
id: link.id as number,
});
}, 5000);
} else {
if (interval) {
clearInterval(interval);
}
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [link.monolith]);
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
const updateLink = useUpdateLink();
const updatePreview = useUpdatePreview();
const submit = async (e?: any) => {
e?.preventDefault();
const { updatedAt: b, ...oldLink } = activeLink;
const { updatedAt: a, ...newLink } = link;
if (JSON.stringify(oldLink) === JSON.stringify(newLink)) {
return;
}
const load = toast.loading(t("updating"));
await updateLink.mutateAsync(link, {
onSettled: (data, error) => {
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
toast.success(t("updated"));
setMode && setMode("view");
setLink(data);
}
},
});
};
const setCollection = (e: any) => {
if (e?.__isNew__) e.value = null;
setLink({
...link,
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
});
};
const setTags = (e: any) => {
const tagNames = e.map((e: any) => ({ name: e.label }));
setLink({ ...link, tags: tagNames });
};
const [iconPopover, setIconPopover] = useState(false);
return (
<div className={clsx(className)} data-vaul-no-drag>
<div
className={clsx(
standalone && "sm:border sm:border-neutral-content sm:rounded-2xl p-5"
)}
>
<div
className={clsx(
"overflow-hidden select-none relative group h-40 opacity-80",
standalone
? "sm:max-w-xl -mx-5 -mt-5 sm:rounded-t-2xl"
: "-mx-4 -mt-4"
)}
>
{previewAvailable(link) ? (
<Image
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true&updatedAt=${link.updatedAt}`}
width={1280}
height={720}
alt=""
className="object-cover scale-105 object-center h-full"
style={{
filter: "blur(1px)",
}}
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
/>
) : link.preview === "unavailable" ? (
<div className="bg-gray-50 duration-100 h-40"></div>
) : (
<div className="duration-100 h-40 skeleton rounded-none"></div>
)}
{!standalone &&
(permissions === true || permissions?.canUpdate) &&
!isPublicRoute && (
<div className="absolute top-0 bottom-0 left-0 right-0 opacity-0 group-hover:opacity-100 duration-100 flex justify-end items-end">
<label className="btn btn-xs mb-2 mr-3 opacity-50 hover:opacity-100">
{t("upload_preview_image")}
<input
type="file"
accept="image/jpg, image/jpeg, image/png"
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
const load = toast.loading(t("updating"));
await updatePreview.mutateAsync(
{
linkId: link.id as number,
file,
},
{
onSettled: (data, error) => {
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
toast.success(t("updated"));
setLink({ updatedAt: data.updatedAt, ...link });
}
},
}
);
}}
className="hidden"
/>
</label>
</div>
)}
</div>
{!standalone &&
(permissions === true || permissions?.canUpdate) &&
!isPublicRoute ? (
<div className="-mt-14 ml-8 relative w-fit pb-2">
<div className="tooltip tooltip-bottom" data-tip={t("change_icon")}>
<LinkIcon
link={link}
className="hover:bg-opacity-70 duration-100 cursor-pointer"
onClick={() => setIconPopover(true)}
/>
</div>
{iconPopover && (
<IconPopover
color={link.color || "#006796"}
setColor={(color: string) => setLink({ ...link, color })}
weight={(link.iconWeight || "regular") as IconWeight}
setWeight={(iconWeight: string) =>
setLink({ ...link, iconWeight })
}
iconName={link.icon as string}
setIconName={(icon: string) => setLink({ ...link, icon })}
reset={() =>
setLink({
...link,
color: "",
icon: "",
iconWeight: "",
})
}
className="top-12"
onClose={() => {
setIconPopover(false);
submit();
}}
/>
)}
</div>
) : (
<div className="-mt-14 ml-8 relative w-fit pb-2">
<LinkIcon link={link} onClick={() => setIconPopover(true)} />
</div>
)}
<div className="max-w-xl sm:px-8 p-5 pb-8 pt-2">
{mode === "view" && (
<div className="text-xl mt-2 pr-7">
<p
className={clsx("relative w-fit", !link.name && "text-neutral")}
>
{unescapeString(link.name) || t("untitled")}
</p>
</div>
)}
{mode === "edit" && (
<>
<br />
<div>
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
{t("name")}
</p>
<TextInput
value={link.name}
onChange={(e) => setLink({ ...link, name: e.target.value })}
placeholder={t("placeholder_example_link")}
className="bg-base-200"
/>
</div>
</>
)}
{link.url && mode === "view" ? (
<>
<br />
<p className="text-sm mb-2 text-neutral">{t("link")}</p>
<div className="relative">
<div className="rounded-md p-2 bg-base-200 hide-scrollbar overflow-x-auto whitespace-nowrap flex justify-between items-center gap-2 pr-14">
<Link href={link.url} title={link.url} target="_blank">
{link.url}
</Link>
<div className="absolute right-0 px-2 bg-base-200">
<CopyButton text={link.url} />
</div>
</div>
</div>
</>
) : activeLink.url ? (
<>
<br />
<div>
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
{t("link")}
</p>
<TextInput
value={link.url || ""}
onChange={(e) => setLink({ ...link, url: e.target.value })}
placeholder={t("placeholder_example_link")}
className="bg-base-200"
/>
</div>
</>
) : undefined}
<br />
<div className="relative">
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
{t("collection")}
</p>
{mode === "view" ? (
<div className="relative">
<Link
href={
isPublicRoute
? `/public/collections/${link.collection.id}`
: `/collections/${link.collection.id}`
}
className="rounded-md p-2 bg-base-200 border border-base-200 hide-scrollbar overflow-x-auto whitespace-nowrap flex justify-between items-center gap-2 pr-14"
>
<p>{link.collection.name}</p>
<div className="absolute right-0 px-2 bg-base-200">
{link.collection.icon ? (
<Icon
icon={link.collection.icon}
size={30}
weight={
(link.collection.iconWeight ||
"regular") as IconWeight
}
color={link.collection.color}
/>
) : (
<i
className="bi-folder-fill text-2xl"
style={{ color: link.collection.color }}
></i>
)}
</div>
</Link>
</div>
) : (
<CollectionSelection
onChange={setCollection}
defaultValue={
link.collection.id
? { value: link.collection.id, label: link.collection.name }
: { value: null as unknown as number, label: "Unorganized" }
}
creatable={false}
/>
)}
</div>
<br />
<div className="relative">
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
{t("tags")}
</p>
{mode === "view" ? (
<div className="flex gap-2 flex-wrap rounded-md p-2 bg-base-200 border border-base-200 w-full text-xs">
{link.tags && link.tags[0] ? (
link.tags.map((tag) =>
isPublicRoute ? (
<div
key={tag.id}
className="bg-base-200 p-1 hover:bg-neutral-content rounded-md duration-100"
>
{tag.name}
</div>
) : (
<Link
href={"/tags/" + tag.id}
key={tag.id}
className="bg-base-200 p-1 hover:bg-neutral-content btn btn-xs btn-ghost rounded-md"
>
{tag.name}
</Link>
)
)
) : (
<div className="text-neutral text-base">{t("no_tags")}</div>
)}
</div>
) : (
<TagSelection
onChange={setTags}
defaultValue={link.tags.map((e) => ({
label: e.name,
value: e.id,
}))}
/>
)}
</div>
<br />
<div className="relative">
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
{t("description")}
</p>
{mode === "view" ? (
<div className="rounded-md p-2 bg-base-200 hyphens-auto">
{link.description ? (
<p>{link.description}</p>
) : (
<p className="text-neutral">{t("no_description_provided")}</p>
)}
</div>
) : (
<textarea
value={unescapeString(link.description) as string}
onChange={(e) =>
setLink({ ...link, description: e.target.value })
}
placeholder={t("link_description_placeholder")}
className="resize-none w-full rounded-md p-2 h-32 border-neutral-content bg-base-200 focus:border-primary border-solid border outline-none duration-100"
/>
)}
</div>
{mode === "view" && (
<div>
<br />
<div className="flex gap-1 items-center mb-2">
<p
className="text-sm text-neutral"
title={t("available_formats")}
>
{link.url ? t("preserved_formats") : t("file")}
</p>
{onUpdateArchive &&
(permissions === true || permissions?.canUpdate) &&
!isPublicRoute && (
<div
className="tooltip tooltip-bottom"
data-tip={t("refresh_preserved_formats")}
>
<button
className="btn btn-xs btn-ghost btn-square text-neutral"
onClick={() => onUpdateArchive()}
>
<i className="bi-arrow-clockwise text-sm" />
</button>
</div>
)}
</div>
<div className={`flex flex-col rounded-md p-3 bg-base-200`}>
{monolithAvailable(link) ? (
<>
<PreservedFormatRow
name={t("webpage")}
icon={"bi-filetype-html"}
format={ArchivedFormat.monolith}
link={link}
downloadable={true}
/>
<hr className="m-3 border-t border-neutral-content" />
</>
) : undefined}
{screenshotAvailable(link) ? (
<>
<PreservedFormatRow
name={t("screenshot")}
icon={"bi-file-earmark-image"}
format={
link?.image?.endsWith("png")
? ArchivedFormat.png
: ArchivedFormat.jpeg
}
link={link}
downloadable={true}
/>
<hr className="m-3 border-t border-neutral-content" />
</>
) : undefined}
{pdfAvailable(link) ? (
<>
<PreservedFormatRow
name={t("pdf")}
icon={"bi-file-earmark-pdf"}
format={ArchivedFormat.pdf}
link={link}
downloadable={true}
/>
<hr className="m-3 border-t border-neutral-content" />
</>
) : undefined}
{readabilityAvailable(link) ? (
<>
<PreservedFormatRow
name={t("readable")}
icon={"bi-file-earmark-text"}
format={ArchivedFormat.readability}
link={link}
/>
<hr className="m-3 border-t border-neutral-content" />
</>
) : undefined}
{!isReady() && !atLeastOneFormatAvailable() ? (
<div
className={`w-full h-full flex flex-col justify-center p-10`}
>
<BeatLoader
color="oklch(var(--p))"
className="mx-auto mb-3"
size={30}
/>
<p className="text-center text-2xl">
{t("preservation_in_queue")}
</p>
<p className="text-center text-lg">
{t("check_back_later")}
</p>
</div>
) : link.url && !isReady() && atLeastOneFormatAvailable() ? (
<div
className={`w-full h-full flex flex-col justify-center p-5`}
>
<BeatLoader
color="oklch(var(--p))"
className="mx-auto mb-3"
size={20}
/>
<p className="text-center">{t("there_are_more_formats")}</p>
<p className="text-center text-sm">
{t("check_back_later")}
</p>
</div>
) : undefined}
{link.url && (
<Link
href={`https://web.archive.org/web/${link?.url?.replace(
/(^\w+:|^)\/\//,
""
)}`}
target="_blank"
className="text-neutral mx-auto duration-100 hover:opacity-60 flex gap-2 w-1/2 justify-center items-center text-sm"
>
<p className="whitespace-nowrap">
{t("view_latest_snapshot")}
</p>
<i className="bi-box-arrow-up-right" />
</Link>
)}
</div>
</div>
)}
{mode === "view" ? (
<>
<br />
<p className="text-neutral text-xs text-center">
{t("saved")}{" "}
{new Date(link.createdAt || "").toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})}{" "}
at{" "}
{new Date(link.createdAt || "").toLocaleTimeString("en-US", {
hour: "numeric",
minute: "numeric",
})}
</p>
</>
) : (
<>
<br />
<div className="flex justify-end items-center">
<button
className={clsx(
"btn btn-accent text-white",
JSON.stringify(activeLink) === JSON.stringify(link)
? "btn-disabled"
: "dark:border-violet-400"
)}
onClick={submit}
>
{t("save_changes")}
</button>
</div>
</>
)}
</div>
</div>
</div>
);
}

View File

@@ -4,207 +4,189 @@ import {
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import usePermissions from "@/hooks/usePermissions";
import EditLinkModal from "@/components/ModalContent/EditLinkModal";
import DeleteLinkModal from "@/components/ModalContent/DeleteLinkModal";
import PreservedFormatsModal from "@/components/ModalContent/PreservedFormatsModal";
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 { useDeleteLink, useGetLink } from "@/hooks/store/links";
import toast from "react-hot-toast";
import LinkModal from "@/components/ModalContent/LinkModal";
import { useRouter } from "next/router";
import clsx from "clsx";
import usePinLink from "@/lib/client/pinLink";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
collection: CollectionIncludingMembersAndLinkCount;
position?: string;
toggleShowInfo?: () => void;
linkInfo?: boolean;
alignToTop?: boolean;
flipDropdown?: boolean;
btnStyle?: string;
};
export default function LinkActions({
link,
toggleShowInfo,
position,
linkInfo,
alignToTop,
flipDropdown,
}: Props) {
export default function LinkActions({ link, btnStyle }: Props) {
const { t } = useTranslation();
const permissions = usePermissions(link.collection.id as number);
const getLink = useGetLink();
const pinLink = usePinLink();
const [editLinkModal, setEditLinkModal] = useState(false);
const [linkModal, setLinkModal] = useState(false);
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
const { data: user = {} } = useUser();
const updateLink = useUpdateLink();
const deleteLink = useDeleteLink();
const pinLink = async () => {
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0] ? true : false;
const updateArchive = async () => {
const load = toast.loading(t("sending_request"));
const load = toast.loading(t("updating"));
const response = await fetch(`/api/v1/links/${link?.id}/archive`, {
method: "PUT",
});
await updateLink.mutateAsync(
{
...link,
pinnedBy: isAlreadyPinned ? undefined : [{ id: user.id }],
},
{
onSettled: (data, error) => {
toast.dismiss(load);
const data = await response.json();
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
toast.success(
isAlreadyPinned ? t("link_unpinned") : t("link_pinned")
);
}
},
}
);
if (response.ok) {
await getLink.mutateAsync({ id: link.id as number });
toast.success(t("link_being_archived"));
} else toast.error(data.response);
};
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
return (
<>
<div
className={`dropdown dropdown-left absolute ${
position || "top-3 right-3"
} ${alignToTop ? "" : "dropdown-end"} z-20`}
>
{isPublicRoute ? (
<div
tabIndex={0}
role="button"
className="absolute top-3 right-3 group-hover:opacity-100 group-focus-within:opacity-100 opacity-0 duration-100"
onMouseDown={dropdownTriggerer}
className="btn btn-ghost btn-sm btn-square text-neutral"
onClick={() => setLinkModal(true)}
>
<i title="More" className="bi-three-dots text-xl" />
<div className={clsx("btn btn-sm btn-square text-neutral", btnStyle)}>
<i title="More" className="bi-info-circle text-xl" />
</div>
</div>
<ul
className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box mr-1 ${
alignToTop ? "" : "translate-y-10"
}`}
) : (
<div
className={`dropdown dropdown-end absolute top-3 right-3 group-hover:opacity-100 group-focus-within:opacity-100 opacity-0 duration-100 z-20`}
>
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
pinLink();
}}
className="whitespace-nowrap"
>
{link?.pinnedBy && link.pinnedBy[0]
? t("unpin")
: t("pin_to_dashboard")}
</div>
</li>
{linkInfo !== undefined && toggleShowInfo ? (
<div
tabIndex={0}
role="button"
onMouseDown={dropdownTriggerer}
className={clsx("btn btn-sm btn-square text-neutral", btnStyle)}
>
<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 mt-1"
}
>
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
toggleShowInfo();
pinLink(link);
}}
className="whitespace-nowrap"
>
{!linkInfo ? t("show_link_details") : t("hide_link_details")}
{link?.pinnedBy && link.pinnedBy[0]
? t("unpin")
: t("pin_to_dashboard")}
</div>
</li>
) : undefined}
{permissions === true || permissions?.canUpdate ? (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setEditLinkModal(true);
setLinkModal(true);
}}
className="whitespace-nowrap"
>
{t("edit_link")}
{t("show_link_details")}
</div>
</li>
) : undefined}
{link.type === "url" && (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setPreservedFormatsModal(true);
}}
className="whitespace-nowrap"
>
{t("preserved_formats")}
</div>
</li>
)}
{permissions === true || permissions?.canDelete ? (
<li>
<div
role="button"
tabIndex={0}
onClick={async (e) => {
(document?.activeElement as HTMLElement)?.blur();
e.shiftKey
? async () => {
const load = toast.loading(t("deleting"));
{(permissions === true || permissions?.canUpdate) && (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setEditLinkModal(true);
}}
className="whitespace-nowrap"
>
{t("edit_link")}
</div>
</li>
)}
{(permissions === true || permissions?.canDelete) && (
<li>
<div
role="button"
tabIndex={0}
onClick={async (e) => {
(document?.activeElement as HTMLElement)?.blur();
console.log(e.shiftKey);
e.shiftKey
? (async () => {
const load = toast.loading(t("deleting"));
await deleteLink.mutateAsync(link.id as number, {
onSettled: (data, error) => {
toast.dismiss(load);
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>
</li>
) : undefined}
</ul>
</div>
{editLinkModal ? (
<EditLinkModal
if (error) {
toast.error(error.message);
} else {
toast.success(t("deleted"));
}
},
});
})()
: setDeleteLinkModal(true);
}}
className="whitespace-nowrap"
>
{t("delete")}
</div>
</li>
)}
</ul>
</div>
)}
{editLinkModal && (
<LinkModal
onClose={() => setEditLinkModal(false)}
activeLink={link}
onPin={() => pinLink(link)}
onUpdateArchive={updateArchive}
onDelete={() => setDeleteLinkModal(true)}
link={link}
activeMode="edit"
/>
) : undefined}
{deleteLinkModal ? (
)}
{deleteLinkModal && (
<DeleteLinkModal
onClose={() => setDeleteLinkModal(false)}
activeLink={link}
/>
) : undefined}
{preservedFormatsModal ? (
<PreservedFormatsModal
onClose={() => setPreservedFormatsModal(false)}
)}
{linkModal && (
<LinkModal
onClose={() => setLinkModal(false)}
onPin={() => pinLink(link)}
onUpdateArchive={updateArchive}
onDelete={() => setDeleteLinkModal(true)}
link={link}
/>
) : undefined}
{/* {expandedLink ? (
<ExpandedLink onClose={() => setExpandedLink(false)} link={link} />
) : undefined} */}
)}
</>
);
}

View File

@@ -3,7 +3,7 @@ import {
CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import useLinkStore from "@/store/links";
import unescapeString from "@/lib/client/unescapeString";
import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions";
@@ -11,7 +11,6 @@ import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate";
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 "./LinkIcon";
import useOnScreen from "@/hooks/useOnScreen";
import { generateLinkHref } from "@/lib/client/generateLinkHref";
@@ -22,24 +21,47 @@ import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import { useGetLink, useLinks } from "@/hooks/store/links";
import { useRouter } from "next/router";
import useLocalSettingsStore from "@/store/localSettings";
import LinkPin from "./LinkPin";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
count: number;
columns: number;
className?: string;
flipDropdown?: boolean;
editMode?: boolean;
};
export default function LinkCard({ link, flipDropdown, editMode }: Props) {
export default function LinkCard({ link, columns, editMode }: Props) {
const { t } = useTranslation();
const heightMap = {
1: "h-44",
2: "h-40",
3: "h-36",
4: "h-32",
5: "h-28",
6: "h-24",
7: "h-20",
8: "h-20",
};
const imageHeightClass = useMemo(
() => (columns ? heightMap[columns as keyof typeof heightMap] : "h-40"),
[columns]
);
const { data: collections = [] } = useCollections();
const { data: user = {} } = useUser();
const { setSelectedLinks, selectedLinks } = useLinkStore();
const {
settings: { show },
} = useLocalSettingsStore();
const {
data: { data: links = [] },
} = useLinks();
@@ -90,8 +112,11 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
const isVisible = useOnScreen(ref);
const permissions = usePermissions(collection?.id as number);
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
useEffect(() => {
let interval: any;
let interval: NodeJS.Timeout | null = null;
if (
isVisible &&
@@ -99,7 +124,10 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
link.preview !== "unavailable"
) {
interval = setInterval(async () => {
getLink.mutateAsync(link.id as number);
getLink.mutateAsync({
id: link.id as number,
isPublicRoute: isPublicRoute,
});
}, 5000);
}
@@ -110,8 +138,6 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
};
}, [isVisible, link.preview]);
const [showInfo, setShowInfo] = useState(false);
const selectedStyle = selectedLinks.some(
(selectedLink) => selectedLink.id === link.id
)
@@ -125,7 +151,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
return (
<div
ref={ref}
className={`${selectedStyle} border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative`}
className={`${selectedStyle} border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative group`}
onClick={() =>
selectable
? handleCheckboxClick(link)
@@ -140,121 +166,76 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
!editMode && window.open(generateLinkHref(link, user), "_blank")
}
>
<div>
<div className="relative rounded-t-2xl h-40 overflow-hidden">
{previewAvailable(link) ? (
<Image
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true`}
width={1280}
height={720}
alt=""
className="rounded-t-2xl select-none object-cover z-10 h-40 w-full shadow opacity-80 scale-105"
style={
link.type !== "image" ? { filter: "blur(1px)" } : undefined
}
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
/>
) : link.preview === "unavailable" ? (
<div className="bg-gray-50 duration-100 h-40 bg-opacity-80"></div>
) : (
<div className="duration-100 h-40 bg-opacity-80 skeleton rounded-none"></div>
)}
{link.type !== "image" && (
<div className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center shadow rounded-md">
<LinkIcon link={link} />
</div>
)}
</div>
<hr className="divider my-0 border-t border-neutral-content h-[1px]" />
</div>
<div className="flex flex-col justify-between h-full">
<div className="p-3 flex flex-col gap-2">
<p className="truncate w-full pr-9 text-primary text-sm">
{unescapeString(link.name)}
</p>
<LinkTypeBadge link={link} />
</div>
{show.image && (
<div>
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
<div className="flex justify-between text-xs text-neutral px-3 pb-1 gap-2">
<div className="cursor-pointer truncate">
{collection && (
<LinkCollection link={link} collection={collection} />
)}
</div>
<LinkDate link={link} />
<div
className={`relative rounded-t-2xl ${imageHeightClass} overflow-hidden`}
>
{previewAvailable(link) ? (
<Image
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true&updatedAt=${link.updatedAt}`}
width={1280}
height={720}
alt=""
className={`rounded-t-2xl select-none object-cover z-10 ${imageHeightClass} w-full shadow opacity-80 scale-105`}
style={show.icon ? { filter: "blur(1px)" } : undefined}
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
/>
) : link.preview === "unavailable" ? (
<div
className={`bg-gray-50 ${imageHeightClass} bg-opacity-80`}
></div>
) : (
<div
className={`${imageHeightClass} bg-opacity-80 skeleton rounded-none`}
></div>
)}
{show.icon && (
<div className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center rounded-md">
<LinkIcon link={link} />
</div>
)}
</div>
<hr className="divider my-0 border-t border-neutral-content h-[1px]" />
</div>
)}
<div className="flex flex-col justify-between h-full min-h-24">
<div className="p-3 flex flex-col gap-2">
{show.name && (
<p className="truncate w-full text-primary text-sm">
{unescapeString(link.name)}
</p>
)}
{show.link && <LinkTypeBadge link={link} />}
</div>
{(show.collection || show.date) && (
<div>
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
<div className="flex justify-between items-center text-xs text-neutral px-3 pb-1 gap-2">
{show.collection && !isPublicRoute && (
<div className="cursor-pointer truncate">
<LinkCollection link={link} collection={collection} />
</div>
)}
{show.date && <LinkDate link={link} />}
</div>
</div>
)}
</div>
</div>
{showInfo && (
<div className="p-3 absolute z-30 top-0 left-0 right-0 bottom-0 bg-base-200 rounded-[0.9rem] fade-in overflow-y-auto">
<div
onClick={() => setShowInfo(!showInfo)}
className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10"
>
<i className="bi-x text-neutral text-2xl"></i>
</div>
<p className="text-neutral text-lg font-semibold">
{t("description")}
</p>
<hr className="divider my-2 border-t border-neutral-content h-[1px]" />
<p>
{link.description ? (
unescapeString(link.description)
) : (
<span className="text-neutral text-sm">
{t("no_description")}
</span>
)}
</p>
{link.tags && link.tags[0] && (
<>
<p className="text-neutral text-lg mt-3 font-semibold">
{t("tags")}
</p>
<hr className="divider my-2 border-t border-neutral-content h-[1px]" />
<div className="flex gap-3 items-center flex-wrap mt-2 truncate relative">
<div className="flex gap-1 items-center flex-wrap">
{link.tags.map((e, i) => (
<Link
href={"/tags/" + e.id}
key={i}
onClick={(e) => {
e.stopPropagation();
}}
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
>
#{e.name}
</Link>
))}
</div>
</div>
</>
)}
</div>
)}
<LinkActions
link={link}
collection={collection}
position="top-[10.75rem] right-3"
toggleShowInfo={() => setShowInfo(!showInfo)}
linkInfo={showInfo}
flipDropdown={flipDropdown}
/>
{/* Overlay on hover */}
<div className="absolute pointer-events-none top-0 left-0 right-0 bottom-0 bg-base-100 bg-opacity-0 group-hover:bg-opacity-20 group-focus-within:opacity-20 rounded-2xl duration-100"></div>
<LinkActions link={link} collection={collection} />
{!isPublicRoute && <LinkPin link={link} />}
</div>
);
}

View File

@@ -3,6 +3,7 @@ import {
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import Link from "next/link";
import { useRouter } from "next/router";
import React from "react";
export default function LinkCollection({
@@ -12,7 +13,11 @@ export default function LinkCollection({
link: LinkIncludingShortenedCollectionAndTags;
collection: CollectionIncludingMembersAndLinkCount;
}) {
return (
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
return !isPublicRoute && collection?.name ? (
<>
<Link
href={`/collections/${link.collection.id}`}
@@ -29,5 +34,5 @@ export default function LinkCollection({
<p className="truncate capitalize">{collection?.name}</p>
</Link>
</>
);
) : null;
}

View File

@@ -1,73 +1,75 @@
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import Image from "next/image";
import isValidUrl from "@/lib/shared/isValidUrl";
import React from "react";
import React, { useState } from "react";
import Icon from "@/components/Icon";
import { IconWeight } from "@phosphor-icons/react";
import clsx from "clsx";
export default function LinkIcon({
link,
className,
size,
hideBackground,
onClick,
}: {
link: LinkIncludingShortenedCollectionAndTags;
className?: string;
size?: "small" | "medium";
hideBackground?: boolean;
onClick?: Function;
}) {
let iconClasses: string =
"bg-white shadow rounded-md border-[2px] flex item-center justify-center border-white select-none z-10 " +
(className || "");
let dimension;
switch (size) {
case "small":
dimension = " w-8 h-8";
break;
case "medium":
dimension = " w-12 h-12";
break;
default:
size = "medium";
dimension = " w-12 h-12";
break;
}
let iconClasses: string = clsx(
"rounded flex item-center justify-center shadow select-none z-10 w-12 h-12",
!hideBackground && "rounded-md bg-white backdrop-blur-lg bg-opacity-50 p-1",
className
);
const url =
isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined;
const [showFavicon, setShowFavicon] = React.useState<boolean>(true);
const [faviconLoaded, setFaviconLoaded] = useState(false);
return (
<>
{link.type === "url" && url ? (
showFavicon ? (
<div onClick={() => onClick && onClick()}>
{link.icon ? (
<div className={iconClasses}>
<Icon
icon={link.icon}
size={30}
weight={(link.iconWeight || "regular") as IconWeight}
color={link.color || "#006796"}
className="m-auto"
/>
</div>
) : link.type === "url" && url ? (
<>
<Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
width={64}
height={64}
alt=""
className={iconClasses + dimension}
className={clsx(
iconClasses,
faviconLoaded ? "" : "absolute opacity-0"
)}
draggable="false"
onError={() => {
setShowFavicon(false);
}}
onLoadingComplete={() => setFaviconLoaded(true)}
onError={() => setFaviconLoaded(false)}
/>
) : (
<LinkPlaceholderIcon
iconClasses={iconClasses + dimension}
size={size}
icon="bi-link-45deg"
/>
)
{!faviconLoaded && (
<LinkPlaceholderIcon
iconClasses={iconClasses}
icon="bi-link-45deg"
/>
)}
</>
) : link.type === "pdf" ? (
<LinkPlaceholderIcon
iconClasses={iconClasses + dimension}
size={size}
iconClasses={iconClasses}
icon="bi-file-earmark-pdf"
/>
) : link.type === "image" ? (
<LinkPlaceholderIcon
iconClasses={iconClasses + dimension}
size={size}
iconClasses={iconClasses}
icon="bi-file-earmark-image"
/>
) : // : link.type === "monolith" ? (
@@ -78,25 +80,19 @@ export default function LinkIcon({
// />
// )
undefined}
</>
</div>
);
}
const LinkPlaceholderIcon = ({
iconClasses,
size,
icon,
}: {
iconClasses: string;
size?: "small" | "medium";
icon: string;
}) => {
return (
<div
className={`${
size === "small" ? "text-2xl" : "text-4xl"
} text-black aspect-square ${iconClasses}`}
>
<div className={clsx(iconClasses, "aspect-square text-4xl text-[#006796]")}>
<i className={`${icon} m-auto`}></i>
</div>
);

View File

@@ -18,20 +18,18 @@ import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import { useLinks } from "@/hooks/store/links";
import useLocalSettingsStore from "@/store/localSettings";
import LinkPin from "./LinkPin";
import { useRouter } from "next/router";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
count: number;
className?: string;
flipDropdown?: boolean;
editMode?: boolean;
};
export default function LinkCardCompact({
link,
flipDropdown,
editMode,
}: Props) {
export default function LinkCardCompact({ link, editMode }: Props) {
const { t } = useTranslation();
const { data: collections = [] } = useCollections();
@@ -39,6 +37,10 @@ export default function LinkCardCompact({
const { data: user = {} } = useUser();
const { setSelectedLinks, selectedLinks } = useLinkStore();
const {
settings: { show },
} = useLocalSettingsStore();
const { links } = useLinks();
useEffect(() => {
@@ -80,8 +82,6 @@ export default function LinkCardCompact({
const permissions = usePermissions(collection?.id as number);
const [showInfo, setShowInfo] = useState(false);
const selectedStyle = selectedLinks.some(
(selectedLink) => selectedLink.id === link.id
)
@@ -92,12 +92,15 @@ export default function LinkCardCompact({
editMode &&
(permissions === true || permissions?.canCreate || permissions?.canDelete);
const router = useRouter();
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
return (
<>
<div
className={`${selectedStyle} border relative items-center flex ${
!showInfo && !isPWA() ? "hover:bg-base-300 p-3" : "py-3"
} duration-200 rounded-lg w-full`}
className={`${selectedStyle} rounded-md border relative group items-center flex ${
!isPWA() ? "hover:bg-base-300 px-2 py-1" : "py-1"
} duration-200 w-full`}
onClick={() =>
selectable
? handleCheckboxClick(link)
@@ -106,67 +109,40 @@ export default function LinkCardCompact({
: undefined
}
>
{/* {showCheckbox &&
editMode &&
(permissions === true ||
permissions?.canCreate ||
permissions?.canDelete) && (
<input
type="checkbox"
className="checkbox checkbox-primary my-auto mr-2"
checked={selectedLinks.some(
(selectedLink) => selectedLink.id === link.id
)}
onChange={() => handleCheckboxClick(link)}
/>
)} */}
<div
className="flex items-center cursor-pointer w-full"
className="flex items-center cursor-pointer w-full min-h-12"
onClick={() =>
!editMode && window.open(generateLinkHref(link, user), "_blank")
}
>
<div className="shrink-0">
<LinkIcon link={link} className="w-12 h-12 text-4xl" />
</div>
{show.icon && (
<div className="shrink-0">
<LinkIcon link={link} hideBackground />
</div>
)}
<div className="w-[calc(100%-56px)] ml-2">
<p className="line-clamp-1 mr-8 text-primary select-none">
{link.name ? (
unescapeString(link.name)
) : (
<div className="mt-2">
<LinkTypeBadge link={link} />
</div>
)}
</p>
{show.name && (
<p className="line-clamp-1 mr-8 text-primary select-none">
{unescapeString(link.name)}
</p>
)}
<div className="mt-1 flex flex-col sm:flex-row sm:items-center gap-2 text-xs text-neutral">
<div className="flex items-center gap-x-3 text-neutral flex-wrap">
{collection ? (
{show.link && <LinkTypeBadge link={link} />}
{show.collection && (
<LinkCollection link={link} collection={collection} />
) : undefined}
{link.name && <LinkTypeBadge link={link} />}
<LinkDate link={link} />
)}
{show.date && <LinkDate link={link} />}
</div>
</div>
</div>
</div>
<LinkActions
link={link}
collection={collection}
position="top-3 right-3"
flipDropdown={flipDropdown}
// toggleShowInfo={() => setShowInfo(!showInfo)}
// linkInfo={showInfo}
/>
{!isPublic && <LinkPin link={link} btnStyle="btn-ghost" />}
<LinkActions link={link} collection={collection} btnStyle="btn-ghost" />
</div>
<div
className="last:hidden rounded-none"
style={{
borderTop: "1px solid var(--fallback-bc,oklch(var(--bc)/0.1))",
}}
></div>
<div className="last:hidden rounded-none my-0 mx-1 border-t border-base-300 h-[1px]"></div>
</>
);
}

View File

@@ -3,7 +3,7 @@ import {
CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import useLinkStore from "@/store/links";
import unescapeString from "@/lib/client/unescapeString";
import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions";
@@ -22,23 +22,45 @@ import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import { useGetLink, useLinks } from "@/hooks/store/links";
import useLocalSettingsStore from "@/store/localSettings";
import clsx from "clsx";
import LinkPin from "./LinkPin";
import { useRouter } from "next/router";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
count: number;
className?: string;
flipDropdown?: boolean;
columns: number;
editMode?: boolean;
};
export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
export default function LinkMasonry({ link, editMode, columns }: Props) {
const { t } = useTranslation();
const heightMap = {
1: "h-44",
2: "h-40",
3: "h-36",
4: "h-32",
5: "h-28",
6: "h-24",
7: "h-20",
8: "h-20",
};
const imageHeightClass = useMemo(
() => (columns ? heightMap[columns as keyof typeof heightMap] : "h-40"),
[columns]
);
const { data: collections = [] } = useCollections();
const { data: user = {} } = useUser();
const { setSelectedLinks, selectedLinks } = useLinkStore();
const {
settings: { show },
} = useLocalSettingsStore();
const { links } = useLinks();
const getLink = useGetLink();
@@ -87,8 +109,12 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
const isVisible = useOnScreen(ref);
const permissions = usePermissions(collection?.id as number);
const router = useRouter();
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
useEffect(() => {
let interval: any;
let interval: NodeJS.Timeout | null = null;
if (
isVisible &&
@@ -96,7 +122,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
link.preview !== "unavailable"
) {
interval = setInterval(async () => {
getLink.mutateAsync(link.id as number);
getLink.mutateAsync({ id: link.id as number });
}, 5000);
}
@@ -107,8 +133,6 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
};
}, [isVisible, link.preview]);
const [showInfo, setShowInfo] = useState(false);
const selectedStyle = selectedLinks.some(
(selectedLink) => selectedLink.id === link.id
)
@@ -122,7 +146,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
return (
<div
ref={ref}
className={`${selectedStyle} border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative`}
className={`${selectedStyle} border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative group`}
onClick={() =>
selectable
? handleCheckboxClick(link)
@@ -137,51 +161,55 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
!editMode && window.open(generateLinkHref(link, user), "_blank")
}
>
<div className="relative rounded-t-2xl overflow-hidden">
{previewAvailable(link) ? (
<Image
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true`}
width={1280}
height={720}
alt=""
className="rounded-t-2xl select-none object-cover z-10 h-40 w-full shadow opacity-80 scale-105"
style={
link.type !== "image" ? { filter: "blur(1px)" } : undefined
}
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
/>
) : link.preview === "unavailable" ? null : (
<div className="duration-100 h-40 bg-opacity-80 skeleton rounded-none"></div>
)}
{link.type !== "image" && (
<div className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center shadow rounded-md">
<LinkIcon link={link} />
{show.image && previewAvailable(link) && (
<div>
<div className="relative rounded-t-2xl overflow-hidden">
{previewAvailable(link) ? (
<Image
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true&updatedAt=${link.updatedAt}`}
width={1280}
height={720}
alt=""
className={`rounded-t-2xl select-none object-cover z-10 ${imageHeightClass} w-full shadow opacity-80 scale-105`}
style={show.icon ? { filter: "blur(1px)" } : undefined}
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
/>
) : link.preview === "unavailable" ? null : (
<div
className={`duration-100 ${imageHeightClass} bg-opacity-80 skeleton rounded-none`}
></div>
)}
{show.icon && (
<div className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center rounded-md">
<LinkIcon link={link} />
</div>
)}
</div>
)}
</div>
{link.preview !== "unavailable" && (
<hr className="divider my-0 last:hidden border-t border-neutral-content h-[1px]" />
<hr className="divider my-0 border-t border-neutral-content h-[1px]" />
</div>
)}
<div className="p-3 flex flex-col gap-2">
<p className="hyphens-auto w-full pr-9 text-primary text-sm">
{unescapeString(link.name)}
</p>
<div className="p-3 flex flex-col gap-2 h-full min-h-14">
{show.name && (
<p className="hyphens-auto w-full text-primary text-sm">
{unescapeString(link.name)}
</p>
)}
<LinkTypeBadge link={link} />
{show.link && <LinkTypeBadge link={link} />}
{link.description && (
<p className="hyphens-auto text-sm">
{show.description && link.description && (
<p className={clsx("hyphens-auto text-sm w-full")}>
{unescapeString(link.description)}
</p>
)}
{link.tags && link.tags[0] && (
{show.tags && link.tags && link.tags[0] && (
<div className="flex gap-1 items-center flex-wrap">
{link.tags.map((e, i) => (
<Link
@@ -199,77 +227,26 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
)}
</div>
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
{(show.collection || show.date) && (
<div>
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
<div className="flex flex-wrap justify-between text-xs text-neutral px-3 pb-1 w-full gap-x-2">
{collection && <LinkCollection link={link} collection={collection} />}
<LinkDate link={link} />
</div>
<div className="flex flex-wrap justify-between items-center text-xs text-neutral px-3 pb-1 w-full gap-x-2">
{!isPublic && show.collection && (
<div className="cursor-pointer truncate">
<LinkCollection link={link} collection={collection} />
</div>
)}
{show.date && <LinkDate link={link} />}
</div>
</div>
)}
</div>
{showInfo && (
<div className="p-3 absolute z-30 top-0 left-0 right-0 bottom-0 bg-base-200 rounded-2xl fade-in overflow-y-auto">
<div
onClick={() => setShowInfo(!showInfo)}
className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10"
>
<i className="bi-x text-neutral text-2xl"></i>
</div>
<p className="text-neutral text-lg font-semibold">
{t("description")}
</p>
<hr className="divider my-2 last:hidden border-t border-neutral-content h-[1px]" />
<p>
{link.description ? (
unescapeString(link.description)
) : (
<span className="text-neutral text-sm">
{t("no_description")}
</span>
)}
</p>
{link.tags && link.tags[0] && (
<>
<p className="text-neutral text-lg mt-3 font-semibold">
{t("tags")}
</p>
<hr className="divider my-2 last:hidden border-t border-neutral-content h-[1px]" />
<div className="flex gap-3 items-center flex-wrap mt-2 truncate relative">
<div className="flex gap-1 items-center flex-wrap">
{link.tags.map((e, i) => (
<Link
href={"/tags/" + e.id}
key={i}
onClick={(e) => {
e.stopPropagation();
}}
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
>
#{e.name}
</Link>
))}
</div>
</div>
</>
)}
</div>
)}
<LinkActions
link={link}
collection={collection}
position={
link.preview !== "unavailable"
? "top-[10.75rem] right-3"
: "top-[.75rem] right-3"
}
toggleShowInfo={() => setShowInfo(!showInfo)}
linkInfo={showInfo}
flipDropdown={flipDropdown}
/>
{/* Overlay on hover */}
<div className="absolute pointer-events-none top-0 left-0 right-0 bottom-0 bg-base-100 bg-opacity-0 group-hover:bg-opacity-20 group-focus-within:opacity-20 rounded-2xl duration-100"></div>
<LinkActions link={link} collection={collection} />
{!isPublic && <LinkPin link={link} />}
</div>
);
}

View File

@@ -32,28 +32,25 @@ export default function Modal({
return (
<Drawer.Root
open={drawerIsOpen}
onClose={() => dismissible && setTimeout(() => toggleModal(), 100)}
onClose={() => dismissible && setDrawerIsOpen(false)}
onAnimationEnd={(isOpen) => !isOpen && toggleModal()}
dismissible={dismissible}
>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<ClickAwayHandler
onClickOutside={() => dismissible && setDrawerIsOpen(false)}
>
<Drawer.Content className="flex flex-col rounded-t-2xl min-h-max mt-24 fixed bottom-0 left-0 right-0 z-30">
<Drawer.Content className="flex flex-col rounded-t-2xl h-[90%] mt-24 fixed bottom-0 left-0 right-0 z-30">
<div
className="p-4 bg-base-100 rounded-t-2xl flex-1 border-neutral-content border-t overflow-y-auto"
data-testid="mobile-modal-container"
>
<div
className="p-4 bg-base-100 rounded-t-2xl flex-1 border-neutral-content border-t overflow-y-auto"
data-testid="mobile-modal-container"
>
<div
className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-neutral mb-5"
data-testid="mobile-modal-slider"
/>
className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-neutral mb-5"
data-testid="mobile-modal-slider"
/>
{children}
</div>
</Drawer.Content>
</ClickAwayHandler>
{children}
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);

View File

@@ -1,7 +1,11 @@
import React, { useEffect, useState } from "react";
import TextInput from "@/components/TextInput";
import toast from "react-hot-toast";
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global";
import {
AccountSettings,
CollectionIncludingMembersAndLinkCount,
Member,
} from "@/types/global";
import getPublicUserData from "@/lib/client/getPublicUserData";
import usePermissions from "@/hooks/usePermissions";
import ProfilePhoto from "../ProfilePhoto";
@@ -11,6 +15,8 @@ import { dropdownTriggerer } from "@/lib/client/utils";
import { useTranslation } from "next-i18next";
import { useUpdateCollection } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import CopyButton from "../CopyButton";
import { useRouter } from "next/router";
type Props = {
onClose: Function;
@@ -40,6 +46,7 @@ export default function EditCollectionSharingModal({
await updateCollection.mutateAsync(collection, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -50,8 +57,6 @@ export default function EditCollectionSharingModal({
}
},
});
setSubmitLoader(false);
}
};
@@ -62,17 +67,11 @@ export default function EditCollectionSharingModal({
const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`;
const [memberUsername, setMemberUsername] = useState("");
const [memberIdentifier, setMemberIdentifier] = useState("");
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
useEffect(() => {
const fetchOwner = async () => {
@@ -93,19 +92,25 @@ export default function EditCollectionSharingModal({
members: [...collection.members, newMember],
});
setMemberUsername("");
setMemberIdentifier("");
};
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">
{permissions === true ? t("share_and_collaborate") : t("team")}
{permissions === true && !isPublicRoute
? t("share_and_collaborate")
: t("team")}
</p>
<div className="divider mb-3 mt-1"></div>
<div className="flex flex-col gap-3">
{permissions === true && (
{permissions === true && !isPublicRoute && (
<div>
<p>{t("make_collection_public")}</p>
@@ -132,43 +137,35 @@ export default function EditCollectionSharingModal({
</div>
)}
{collection.isPublic ? (
<div className={permissions === true ? "pl-5" : ""}>
<p className="mb-2">{t("sharable_link_guide")}</p>
<div
onClick={() => {
try {
navigator.clipboard
.writeText(publicCollectionURL)
.then(() => toast.success(t("copied")));
} catch (err) {
console.log(err);
}
}}
className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border outline-none hover:border-primary dark:hover:border-primary duration-100 cursor-text"
>
{collection.isPublic && (
<div>
<p className="mb-2">{t("sharable_link")}</p>
<div className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border flex items-center gap-2 justify-between">
{publicCollectionURL}
<CopyButton text={publicCollectionURL} />
</div>
</div>
) : null}
)}
{permissions === true && <div className="divider my-3"></div>}
{permissions === true && !isPublicRoute && (
<div className="divider my-3"></div>
)}
{permissions === true && (
{permissions === true && !isPublicRoute && (
<>
<p>{t("members")}</p>
<div className="flex items-center gap-2">
<TextInput
value={memberUsername || ""}
value={memberIdentifier || ""}
className="bg-base-200"
placeholder={t("members_username_placeholder")}
onChange={(e) => setMemberUsername(e.target.value)}
placeholder={t("add_member_placeholder")}
onChange={(e) => setMemberIdentifier(e.target.value)}
onKeyDown={(e) =>
e.key === "Enter" &&
addMemberToCollection(
user.username as string,
memberUsername || "",
user,
memberIdentifier.replace(/^@/, "") || "",
collection,
setMemberState,
t
@@ -179,8 +176,8 @@ export default function EditCollectionSharingModal({
<div
onClick={() =>
addMemberToCollection(
user.username as string,
memberUsername || "",
user,
memberIdentifier.replace(/^@/, "") || "",
collection,
setMemberState,
t
@@ -266,7 +263,7 @@ export default function EditCollectionSharingModal({
</div>
<div className={"flex items-center gap-2"}>
{permissions === true ? (
{permissions === true && !isPublicRoute ? (
<div className="dropdown dropdown-bottom dropdown-end">
<div
tabIndex={0}
@@ -427,7 +424,7 @@ export default function EditCollectionSharingModal({
</p>
)}
{permissions === true && (
{permissions === true && !isPublicRoute && (
<i
className={
"bi-x text-xl btn btn-sm btn-square btn-ghost text-neutral hover:text-red-500 dark:hover:text-red-500 duration-100 cursor-pointer"
@@ -458,7 +455,7 @@ export default function EditCollectionSharingModal({
</>
)}
{permissions === true && (
{permissions === true && !isPublicRoute && (
<button
className="btn btn-accent dark:border-violet-400 text-white w-fit ml-auto mt-3"
onClick={submit}

View File

@@ -0,0 +1,131 @@
import toast from "react-hot-toast";
import Modal from "../Modal";
import TextInput from "../TextInput";
import { FormEvent, useState } from "react";
import { useTranslation, Trans } from "next-i18next";
import { useAddUser } from "@/hooks/store/admin/users";
import Link from "next/link";
import { signIn } from "next-auth/react";
type Props = {
onClose: Function;
};
type FormData = {
username?: string;
email?: string;
invite: boolean;
};
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true";
export default function InviteModal({ onClose }: Props) {
const { t } = useTranslation();
const addUser = useAddUser();
const [form, setForm] = useState<FormData>({
username: emailEnabled ? undefined : "",
email: emailEnabled ? "" : undefined,
invite: true,
});
const [submitLoader, setSubmitLoader] = useState(false);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!submitLoader) {
const checkFields = () => {
if (emailEnabled) {
return form.email !== "";
} else {
return form.username !== "";
}
};
if (checkFields()) {
setSubmitLoader(true);
await addUser.mutateAsync(form, {
onSettled: () => {
setSubmitLoader(false);
},
onSuccess: async () => {
await signIn("invite", {
email: form.email,
callbackUrl: "/member-onboarding",
redirect: false,
});
onClose();
},
});
} else {
toast.error(t("fill_all_fields_error"));
}
}
}
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">{t("invite_user")}</p>
<div className="divider mb-3 mt-1"></div>
<p className="mb-3">{t("invite_user_desc")}</p>
<form onSubmit={submit}>
{emailEnabled ? (
<div>
<TextInput
placeholder={t("placeholder_email")}
className="bg-base-200"
onChange={(e) => setForm({ ...form, email: e.target.value })}
value={form.email}
/>
</div>
) : (
<div>
<p className="mb-2">
{t("username")}{" "}
{emailEnabled && (
<span className="text-xs text-neutral">{t("optional")}</span>
)}
</p>
<TextInput
placeholder={t("placeholder_john")}
className="bg-base-200"
onChange={(e) => setForm({ ...form, username: e.target.value })}
value={form.username}
/>
</div>
)}
<div role="note" className="alert alert-note mt-5">
<i className="bi-exclamation-triangle text-xl" />
<span>
<p>{t("invite_user_note")}</p>
<p className="mb-1">
{t("invite_user_price", {
price: 4,
priceAnnual: 36,
})}
</p>
<Link
href="https://docs.linkwarden.app/billing/seats#how-seats-affect-billing"
className="font-semibold whitespace-nowrap hover:opacity-80 duration-100"
target="_blank"
>
{t("learn_more")} <i className="bi-box-arrow-up-right"></i>
</Link>
</span>
</div>
<div className="flex justify-between items-center mt-5">
<button
className="btn btn-accent dark:border-violet-400 text-white ml-auto"
type="submit"
>
{t("send_invitation")}
</button>
</div>
</form>
</Modal>
);
}

View File

@@ -0,0 +1,186 @@
import React, { useState } from "react";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { useTranslation } from "next-i18next";
import { useDeleteLink } from "@/hooks/store/links";
import Drawer from "../Drawer";
import LinkDetails from "../LinkDetails";
import Link from "next/link";
import usePermissions from "@/hooks/usePermissions";
import { useRouter } from "next/router";
import { dropdownTriggerer } from "@/lib/client/utils";
import toast from "react-hot-toast";
import clsx from "clsx";
type Props = {
onClose: Function;
onDelete: Function;
onUpdateArchive: Function;
onPin: Function;
link: LinkIncludingShortenedCollectionAndTags;
activeMode?: "view" | "edit";
};
export default function LinkModal({
onClose,
onDelete,
onUpdateArchive,
onPin,
link,
activeMode,
}: Props) {
const { t } = useTranslation();
const permissions = usePermissions(link.collection.id as number);
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
const deleteLink = useDeleteLink();
const [mode, setMode] = useState<"view" | "edit">(activeMode || "view");
return (
<Drawer
toggleDrawer={onClose}
className="sm:h-screen items-center relative"
>
<div className="absolute top-3 left-0 right-0 flex justify-between px-3">
<div
className="bi-x text-xl btn btn-sm btn-circle text-base-content opacity-50 hover:opacity-100 z-10"
onClick={() => onClose()}
></div>
{(permissions === true || permissions?.canUpdate) && !isPublicRoute && (
<div className="flex gap-1 h-8 rounded-full bg-neutral-content bg-opacity-50 text-base-content p-1 text-xs duration-100 select-none z-10">
<div
className={clsx(
"py-1 px-2 cursor-pointer duration-100 rounded-full font-semibold",
mode === "view" && "bg-primary bg-opacity-50"
)}
onClick={() => {
setMode("view");
}}
>
View
</div>
<div
className={clsx(
"py-1 px-2 cursor-pointer duration-100 rounded-full font-semibold",
mode === "edit" && "bg-primary bg-opacity-50"
)}
onClick={() => {
setMode("edit");
}}
>
Edit
</div>
</div>
)}
<div className="flex gap-2">
{!isPublicRoute && (
<div className={`dropdown dropdown-end z-20`}>
<div
tabIndex={0}
role="button"
onMouseDown={dropdownTriggerer}
className="btn btn-sm btn-circle text-base-content opacity-50 hover:opacity-100 z-10"
>
<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`}
>
{
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
onPin();
}}
className="whitespace-nowrap"
>
{link?.pinnedBy && link.pinnedBy[0]
? t("unpin")
: t("pin_to_dashboard")}
</div>
</li>
}
{link.type === "url" &&
(permissions === true || permissions?.canUpdate) && (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
onUpdateArchive();
}}
className="whitespace-nowrap"
>
{t("refresh_preserved_formats")}
</div>
</li>
)}
{(permissions === true || permissions?.canDelete) && (
<li>
<div
role="button"
tabIndex={0}
onClick={async (e) => {
(document?.activeElement as HTMLElement)?.blur();
console.log(e.shiftKey);
if (e.shiftKey) {
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"));
}
},
});
onClose();
} else {
onDelete();
onClose();
}
}}
className="whitespace-nowrap"
>
{t("delete")}
</div>
</li>
)}
</ul>
</div>
)}
{link.url && (
<Link
href={link.url}
target="_blank"
className="bi-box-arrow-up-right btn-circle text-base-content opacity-50 hover:opacity-100 btn btn-sm select-none z-10"
></Link>
)}
</div>
</div>
<div className="w-full">
<LinkDetails
activeLink={link}
className="sm:mt-0 -mt-11"
mode={mode}
setMode={(mode: "view" | "edit") => setMode(mode)}
onUpdateArchive={onUpdateArchive}
/>
</div>
</Drawer>
);
}

View File

@@ -0,0 +1,67 @@
import React, { useState } from "react";
import Modal from "../Modal";
import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
submit: Function;
};
export default function SurveyModal({ onClose, submit }: Props) {
const { t } = useTranslation();
const [referer, setReferrer] = useState("rather_not_say");
const [other, setOther] = useState("");
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">{t("quick_survey")}</p>
<div className="divider mb-3 mt-1"></div>
<div className="flex flex-col gap-4">
<p>{t("how_did_you_discover_linkwarden")}</p>
<select
onChange={(e) => {
setReferrer(e.target.value);
setOther("");
}}
className="select border border-neutral-content focus:outline-none focus:border-primary duration-100 w-full bg-base-200 rounded-[0.375rem] min-h-0 h-[2.625rem] leading-4 p-2"
>
<option value="rather_not_say">{t("rather_not_say")}</option>
<option value="search_engine">{t("search_engine")}</option>
<option value="people_recommendation">
{t("people_recommendation")}
</option>
<option value="reddit">{t("reddit")}</option>
<option value="github">{t("github")}</option>
<option value="twitter">{t("twitter")}</option>
<option value="mastodon">{t("mastodon")}</option>
<option value="lemmy">{t("lemmy")}</option>
<option value="other">{t("other")}</option>
</select>
{referer === "other" && (
<input
type="text"
placeholder={t("please_specify")}
onChange={(e) => {
setOther(e.target.value);
}}
value={other}
className="input border border-neutral-content focus:border-primary focus:outline-none duration-100 w-full bg-base-200 rounded-[0.375rem] min-h-0 h-[2.625rem] leading-4 p-2"
/>
)}
<Button
className="ml-auto mt-3"
intent="accent"
onClick={() => submit(referer, other)}
>
{t("submit")}
</Button>
</div>
</Modal>
);
}

View File

@@ -60,7 +60,7 @@ export default function ProfileDropdown() {
})}
</div>
</li>
{isAdmin ? (
{isAdmin && (
<li>
<Link
href="/admin"
@@ -72,7 +72,7 @@ export default function ProfileDropdown() {
{t("server_administration")}
</Link>
</li>
) : null}
)}
<li>
<div
onClick={() => {

View File

@@ -1,7 +1,8 @@
import React, { Dispatch, SetStateAction, useEffect } from "react";
import useLocalSettingsStore from "@/store/localSettings";
import { ViewMode } from "@/types/global";
import { dropdownTriggerer } from "@/lib/client/utils";
import { useTranslation } from "next-i18next";
type Props = {
viewMode: ViewMode;
@@ -9,64 +10,138 @@ type Props = {
};
export default function ViewDropdown({ viewMode, setViewMode }: Props) {
const { updateSettings } = useLocalSettingsStore();
const onChangeViewMode = (
e: React.MouseEvent<HTMLButtonElement>,
viewMode: ViewMode
) => {
setViewMode(viewMode);
};
const { settings, updateSettings } = useLocalSettingsStore((state) => state);
const { t } = useTranslation();
useEffect(() => {
updateSettings({ viewMode });
}, [viewMode]);
}, [viewMode, updateSettings]);
const onChangeViewMode = (mode: ViewMode) => {
setViewMode(mode);
updateSettings({ viewMode });
};
const toggleShowSetting = (setting: keyof typeof settings.show) => {
const newShowSettings = {
...settings.show,
[setting]: !settings.show[setting],
};
updateSettings({ show: newShowSettings });
};
const onColumnsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
updateSettings({ columns: Number(e.target.value) });
};
return (
<div className="p-1 flex flex-row gap-1 border border-neutral-content rounded-[0.625rem]">
<button
onClick={(e) => onChangeViewMode(e, ViewMode.Card)}
className={`btn btn-square btn-sm btn-ghost ${
viewMode == ViewMode.Card
? "bg-primary/20 hover:bg-primary/20"
: "hover:bg-neutral/20"
}`}
<div className="dropdown dropdown-bottom dropdown-end">
<div
tabIndex={0}
role="button"
onMouseDown={dropdownTriggerer}
className="btn btn-sm btn-square btn-ghost border-none"
>
<i className="bi-grid w-4 h-4 text-neutral"></i>
</button>
<button
onClick={(e) => onChangeViewMode(e, ViewMode.Masonry)}
className={`btn btn-square btn-sm btn-ghost ${
viewMode == ViewMode.Masonry
? "bg-primary/20 hover:bg-primary/20"
: "hover:bg-neutral/20"
}`}
{viewMode === ViewMode.Card ? (
<i className="bi-grid w-4 h-4 text-neutral"></i>
) : viewMode === ViewMode.Masonry ? (
<i className="bi-columns-gap w-4 h-4 text-neutral"></i>
) : (
<i className="bi-view-stacked w-4 h-4 text-neutral"></i>
)}
</div>
<ul
tabIndex={0}
className="dropdown-content z-[30] menu shadow bg-base-200 min-w-52 border border-neutral-content rounded-xl mt-1"
>
<i className="bi bi-columns-gap w-4 h-4 text-neutral"></i>
</button>
<button
onClick={(e) => onChangeViewMode(e, ViewMode.List)}
className={`btn btn-square btn-sm btn-ghost ${
viewMode == ViewMode.List
? "bg-primary/20 hover:bg-primary/20"
: "hover:bg-neutral/20"
}`}
>
<i className="bi bi-view-stacked w-4 h-4 text-neutral"></i>
</button>
{/* <button
onClick={(e) => onChangeViewMode(e, ViewMode.Grid)}
className={`btn btn-square btn-sm btn-ghost ${
viewMode == ViewMode.Grid
? "bg-primary/20 hover:bg-primary/20"
: "hover:bg-neutral/20"
}`}
>
<i className="bi-columns-gap w-4 h-4 text-neutral"></i>
</button> */}
<p className="mb-1 text-sm text-neutral">{t("view")}</p>
<div className="p-1 flex w-full justify-between gap-1 border border-neutral-content rounded-[0.625rem]">
<button
onClick={(e) => onChangeViewMode(ViewMode.Card)}
className={`btn w-[31%] btn-sm btn-ghost ${
viewMode === ViewMode.Card
? "bg-primary/20 hover:bg-primary/20"
: "hover:bg-neutral/20"
}`}
>
<i className="bi-grid text-lg text-neutral"></i>
</button>
<button
onClick={(e) => onChangeViewMode(ViewMode.Masonry)}
className={`btn w-[31%] btn-sm btn-ghost ${
viewMode === ViewMode.Masonry
? "bg-primary/20 hover:bg-primary/20"
: "hover:bg-neutral/20"
}`}
>
<i className="bi-columns-gap text-lg text-neutral"></i>
</button>
<button
onClick={(e) => onChangeViewMode(ViewMode.List)}
className={`btn w-[31%] btn-sm btn-ghost ${
viewMode === ViewMode.List
? "bg-primary/20 hover:bg-primary/20"
: "hover:bg-neutral/20"
}`}
>
<i className="bi-view-stacked text-lg text-neutral"></i>
</button>
</div>
<p className="mb-1 mt-2 text-sm text-neutral">{t("show")}</p>
{Object.entries(settings.show)
.filter((e) =>
settings.viewMode === ViewMode.List // Hide tags, image, and description checkboxes in list view
? e[0] !== "tags" && e[0] !== "image" && e[0] !== "description"
: settings.viewMode === ViewMode.Card // Hide tags and description checkboxes in card view
? e[0] !== "tags" && e[0] !== "description"
: true
)
.map(([key, value]) => (
<li key={key}>
<label className="label cursor-pointer flex justify-start">
<input
type="checkbox"
className="checkbox checkbox-primary"
checked={value}
onChange={() =>
toggleShowSetting(key as keyof typeof settings.show)
}
/>
<span className="label-text whitespace-nowrap">{t(key)}</span>
</label>
</li>
))}
{settings.viewMode !== ViewMode.List && (
<>
<p className="mb-1 mt-2 text-sm text-neutral">
{t("columns")}:{" "}
{settings.columns === 0 ? t("default") : settings.columns}
</p>
<div>
<input
type="range"
min={0}
max="8"
value={settings.columns}
onChange={(e) => onColumnsChange(e)}
className="range range-xs range-primary"
step="1"
/>
<div className="flex w-full justify-between px-2 text-xs text-neutral select-none">
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
<span>|</span>
</div>
</div>
</>
)}
</ul>
</div>
);
}

17
components/ui/Divider.tsx Normal file
View File

@@ -0,0 +1,17 @@
import clsx from "clsx";
import React from "react";
type Props = {
className?: string;
vertical?: boolean;
};
function Divider({ className, vertical = false }: Props) {
return vertical ? (
<hr className={clsx("border-neutral-content border-l h-full", className)} />
) : (
<hr className={clsx("border-neutral-content border-t", className)} />
);
}
export default Divider;

View File

@@ -0,0 +1,29 @@
import { Tag } from "@prisma/client";
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
type TagIncludingCount = Tag & { _count: { links: number } };
const usePublicTags = (): UseQueryResult<TagIncludingCount[]> => {
const { status } = useSession();
const router = useRouter();
return useQuery({
queryKey: ["tags"],
queryFn: async () => {
const response = await fetch(
"/api/v1/public/collections/tags" +
"?collectionId=" +
router.query.id || ""
);
if (!response.ok) throw new Error("Failed to fetch tags.");
const data = await response.json();
return data.response;
},
});
};
export { usePublicTags };

View File

@@ -1,46 +1,68 @@
import { prisma } from "@/lib/api/db";
export default async function getTags(userId: number) {
// Remove empty tags
await prisma.tag.deleteMany({
where: {
ownerId: userId,
links: {
none: {},
export default async function getTags({
userId,
collectionId,
}: {
userId?: number;
collectionId?: number;
}) {
if (userId) {
// Remove empty tags
await prisma.tag.deleteMany({
where: {
ownerId: userId,
links: {
none: {},
},
},
},
});
});
const tags = await prisma.tag.findMany({
where: {
OR: [
{ ownerId: userId }, // Tags owned by the user
{
links: {
some: {
collection: {
members: {
some: {
userId, // Tags from collections where the user is a member
const tags = await prisma.tag.findMany({
where: {
OR: [
{ ownerId: userId }, // Tags owned by the user
{
links: {
some: {
collection: {
members: {
some: {
userId, // Tags from collections where the user is a member
},
},
},
},
},
},
},
],
},
include: {
_count: {
select: { links: true },
],
},
},
// orderBy: {
// links: {
// _count: "desc",
// },
// },
});
include: {
_count: {
select: { links: true },
},
},
// orderBy: {
// links: {
// _count: "desc",
// },
// },
});
return { response: tags, status: 200 };
return { response: tags, status: 200 };
} else if (collectionId) {
const tags = await prisma.tag.findMany({
where: {
links: {
some: {
collection: {
id: collectionId,
},
},
},
},
});
return { response: tags, status: 200 };
}
}

View File

@@ -1,7 +1,9 @@
import { prisma } from "@/lib/api/db";
import type { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcrypt";
import isServerAdmin from "../../isServerAdmin";
import { PostUserSchema } from "@/lib/shared/schemaValidation";
import isAuthenticatedRequest from "../../isAuthenticatedRequest";
import { Subscription, User } from "@prisma/client";
const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
@@ -12,66 +14,59 @@ interface Data {
status: number;
}
interface User {
name: string;
username?: string;
email?: string;
password: string;
}
export default async function postUser(
req: NextApiRequest,
res: NextApiResponse
): Promise<Data> {
let isAdmin = await isServerAdmin({ req });
const parentUser = await isAuthenticatedRequest({ req });
const isAdmin =
parentUser && parentUser.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1);
if (process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" && !isAdmin) {
return { response: "Registration is disabled.", status: 400 };
}
const body: User = req.body;
const dataValidation = PostUserSchema().safeParse(req.body);
const checkHasEmptyFields = emailEnabled
? !body.password || !body.name || !body.email
: !body.username || !body.password || !body.name;
if (!dataValidation.success) {
return {
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
status: 400,
};
}
if (!body.password || body.password.length < 8)
return { response: "Password must be at least 8 characters.", status: 400 };
const { name, email, password, invite } = dataValidation.data;
let { username } = dataValidation.data;
if (checkHasEmptyFields)
return { response: "Please fill out all the fields.", status: 400 };
// Check email (if enabled)
const checkEmail =
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
if (emailEnabled && !checkEmail.test(body.email?.toLowerCase() || ""))
return { response: "Please enter a valid email.", status: 400 };
// Check username (if email was disabled)
const checkUsername = RegExp("^[a-z0-9_-]{3,31}$");
if (invite && (!stripeEnabled || !emailEnabled)) {
return { response: "You are not authorized to invite users.", status: 401 };
} else if (invite && !parentUser) {
return { response: "You must be logged in to invite users.", status: 401 };
}
const autoGeneratedUsername = "user" + Math.round(Math.random() * 1000000000);
if (body.username && !checkUsername.test(body.username?.toLowerCase()))
if (!username) {
username = autoGeneratedUsername;
}
if (!emailEnabled && !password) {
return {
response:
"Username has to be between 3-30 characters, no spaces and special characters are allowed.",
response: "Password is required.",
status: 400,
};
else if (!body.username) {
body.username = autoGeneratedUsername;
}
const checkIfUserExists = await prisma.user.findFirst({
where: {
OR: [
{
email: body.email ? body.email.toLowerCase().trim() : undefined,
email: email ? email.toLowerCase().trim() : undefined,
},
{
username: body.username
? body.username.toLowerCase().trim()
: undefined,
username: username ? username.toLowerCase().trim() : undefined,
},
],
},
@@ -83,65 +78,57 @@ export default async function postUser(
const saltRounds = 10;
const hashedPassword = bcrypt.hashSync(body.password, saltRounds);
const hashedPassword = bcrypt.hashSync(password || "", saltRounds);
// Subscription dates
const currentPeriodStart = new Date();
const currentPeriodEnd = new Date();
currentPeriodEnd.setFullYear(currentPeriodEnd.getFullYear() + 1000); // end date is in 1000 years...
if (isAdmin) {
const user = await prisma.user.create({
data: {
name: body.name,
username: emailEnabled
? (body.username as string).toLowerCase().trim() ||
autoGeneratedUsername
: (body.username as string).toLowerCase().trim(),
email: emailEnabled ? body.email?.toLowerCase().trim() : undefined,
password: hashedPassword,
emailVerified: new Date(),
subscriptions: stripeEnabled
const user = await prisma.user.create({
data: {
name: name,
username: emailEnabled ? username || autoGeneratedUsername : username,
email: emailEnabled ? email : undefined,
emailVerified: isAdmin ? new Date() : undefined,
password: password ? hashedPassword : undefined,
parentSubscription:
parentUser && invite
? {
connect: {
id: (parentUser.subscriptions as Subscription).id,
},
}
: undefined,
subscriptions:
stripeEnabled && isAdmin
? {
create: {
stripeSubscriptionId:
"fake_sub_" + Math.round(Math.random() * 10000000000000),
active: true,
currentPeriodStart,
currentPeriodEnd,
currentPeriodStart: new Date(),
currentPeriodEnd: new Date(
new Date().setFullYear(new Date().getFullYear() + 1000)
), // 1000 years from now
},
}
: undefined,
},
select: {
id: true,
username: true,
email: true,
emailVerified: true,
subscriptions: {
select: {
active: true,
},
select: isAdmin
? {
id: true,
username: true,
email: true,
emailVerified: true,
password: true,
subscriptions: {
select: {
active: true,
},
},
},
createdAt: true,
},
});
createdAt: true,
}
: undefined,
});
return { response: user, status: 201 };
} else {
await prisma.user.create({
data: {
name: body.name,
username: emailEnabled
? autoGeneratedUsername
: (body.username as string).toLowerCase().trim(),
email: emailEnabled ? body.email?.toLowerCase().trim() : undefined,
password: hashedPassword,
},
});
return { response: "User successfully created.", status: 201 };
}
const { password: pass, ...userWithoutPassword } = user as User;
return { response: userWithoutPassword, status: 201 };
} else {
return { response: "Email or Username already exists.", status: 400 };
}

View File

@@ -4,15 +4,28 @@ import removeFolder from "@/lib/api/storage/removeFolder";
import Stripe from "stripe";
import { DeleteUserBody } from "@/types/global";
import removeFile from "@/lib/api/storage/removeFile";
import updateSeats from "@/lib/api/stripe/updateSeats";
export default async function deleteUserById(
userId: number,
body: DeleteUserBody,
isServerAdmin?: boolean
isServerAdmin: boolean,
queryId: number
) {
// First, we retrieve the user from the database
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
subscriptions: {
include: {
user: true,
},
},
parentSubscription: {
include: {
user: true,
},
},
},
});
if (!user) {
@@ -23,24 +36,70 @@ export default async function deleteUserById(
}
if (!isServerAdmin) {
if (user.password) {
const isPasswordValid = bcrypt.compareSync(
body.password,
user.password as string
);
if (queryId === userId) {
if (user.password) {
const isPasswordValid = bcrypt.compareSync(
body.password,
user.password
);
if (!isPasswordValid && !isServerAdmin) {
if (!isPasswordValid && !isServerAdmin) {
return {
response: "Invalid credentials.",
status: 401,
};
}
} else {
return {
response: "Invalid credentials.",
status: 401, // Unauthorized
response:
"User has no password. Please reset your password from the forgot password page.",
status: 401,
};
}
} else {
return {
response:
"User has no password. Please reset your password from the forgot password page.",
status: 401, // Unauthorized
};
if (user.parentSubscriptionId) {
return {
response: "Permission denied.",
status: 401,
};
} else {
if (!user.subscriptions) {
return {
response: "User has no subscription.",
status: 401,
};
}
const findChild = await prisma.user.findFirst({
where: { id: queryId, parentSubscriptionId: user.subscriptions?.id },
});
if (!findChild)
return {
response: "Permission denied.",
status: 401,
};
const removeUser = await prisma.user.update({
where: { id: findChild.id },
data: {
parentSubscription: {
disconnect: true,
},
},
});
if (removeUser.emailVerified)
await updateSeats(
user.subscriptions.stripeSubscriptionId,
user.subscriptions.quantity - 1
);
return {
response: "Account removed from subscription.",
status: 200,
};
}
}
}
@@ -50,27 +109,27 @@ export default async function deleteUserById(
async (prisma) => {
// Delete Access Tokens
await prisma.accessToken.deleteMany({
where: { userId },
where: { userId: queryId },
});
// Delete whitelisted users
await prisma.whitelistedUser.deleteMany({
where: { userId },
where: { userId: queryId },
});
// Delete links
await prisma.link.deleteMany({
where: { collection: { ownerId: userId } },
where: { collection: { ownerId: queryId } },
});
// Delete tags
await prisma.tag.deleteMany({
where: { ownerId: userId },
where: { ownerId: queryId },
});
// Find collections that the user owns
const collections = await prisma.collection.findMany({
where: { ownerId: userId },
where: { ownerId: queryId },
});
for (const collection of collections) {
@@ -89,29 +148,29 @@ export default async function deleteUserById(
// Delete collections after cleaning up related data
await prisma.collection.deleteMany({
where: { ownerId: userId },
where: { ownerId: queryId },
});
// Delete subscription
if (process.env.STRIPE_SECRET_KEY)
await prisma.subscription
.delete({
where: { userId },
where: { userId: queryId },
})
.catch((err) => console.log(err));
await prisma.usersAndCollections.deleteMany({
where: {
OR: [{ userId: userId }, { collection: { ownerId: userId } }],
OR: [{ userId: queryId }, { collection: { ownerId: queryId } }],
},
});
// Delete user's avatar
await removeFile({ filePath: `uploads/avatar/${userId}.jpg` });
await removeFile({ filePath: `uploads/avatar/${queryId}.jpg` });
// Finally, delete the user
await prisma.user.delete({
where: { id: userId },
where: { id: queryId },
});
},
{ timeout: 20000 }
@@ -124,24 +183,36 @@ export default async function deleteUserById(
});
try {
const listByEmail = await stripe.customers.list({
email: user.email?.toLowerCase(),
expand: ["data.subscriptions"],
});
if (user.subscriptions?.id) {
const listByEmail = await stripe.customers.list({
email: user.email?.toLowerCase(),
expand: ["data.subscriptions"],
});
if (listByEmail.data[0].subscriptions?.data[0].id) {
const deleted = await stripe.subscriptions.cancel(
listByEmail.data[0].subscriptions?.data[0].id,
{
cancellation_details: {
comment: body.cancellation_details?.comment,
feedback: body.cancellation_details?.feedback,
},
}
if (listByEmail.data[0].subscriptions?.data[0].id) {
const deleted = await stripe.subscriptions.cancel(
listByEmail.data[0].subscriptions?.data[0].id,
{
cancellation_details: {
comment: body.cancellation_details?.comment,
feedback: body.cancellation_details?.feedback,
},
}
);
return {
response: deleted,
status: 200,
};
}
} else if (user.parentSubscription?.id && user && user.emailVerified) {
await updateSeats(
user.parentSubscription.stripeSubscriptionId,
user.parentSubscription.quantity - 1
);
return {
response: deleted,
response: "User account and all related data deleted successfully.",
status: 200,
};
}

View File

@@ -212,6 +212,8 @@ export default async function updateUserById(
archiveAsWaybackMachine: data.archiveAsWaybackMachine,
linksRouteTo: data.linksRouteTo,
preventDuplicateLinks: data.preventDuplicateLinks,
referredBy:
!user?.referredBy && data.referredBy ? data.referredBy : undefined,
password:
data.newPassword && data.newPassword !== ""
? newHashedPassword

View File

@@ -39,7 +39,7 @@ const handleMonolith = async (link: Link, content: string) => {
});
});
} catch (err) {
console.log("Error running MONOLITH:", err);
console.log("Uncaught Monolith error...");
}
};

View File

@@ -0,0 +1,211 @@
import { ArchivedFormat, TokenExpiry } from "@/types/global";
import { LinksRouteTo } from "@prisma/client";
import { z } from "zod";
// const stringField = z.string({
// errorMap: (e) => ({
// message: `Invalid ${e.path}.`,
// }),
// });
export const ForgotPasswordSchema = z.object({
email: z.string().email(),
});
export const ResetPasswordSchema = z.object({
token: z.string(),
password: z.string().min(8),
});
export const VerifyEmailSchema = z.object({
token: z.string(),
});
export const PostTokenSchema = z.object({
name: z.string().max(50),
expires: z.nativeEnum(TokenExpiry),
});
export type PostTokenSchemaType = z.infer<typeof PostTokenSchema>;
export const PostUserSchema = () => {
const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
return z.object({
name: z.string().trim().min(1).max(50).optional(),
password: z.string().min(8).max(2048).optional(),
email: emailEnabled
? z.string().trim().email().toLowerCase()
: z.string().optional(),
username: emailEnabled
? z.string().optional()
: z
.string()
.trim()
.toLowerCase()
.min(3)
.max(50)
.regex(/^[a-z0-9_-]{3,50}$/),
invite: z.boolean().optional(),
});
};
export const UpdateUserSchema = () => {
const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
return z.object({
name: z.string().trim().min(1).max(50).optional(),
email: emailEnabled
? z.string().trim().email().toLowerCase()
: z.string().optional(),
username: z
.string()
.trim()
.toLowerCase()
.min(3)
.max(30)
.regex(/^[a-z0-9_-]{3,30}$/),
image: z.string().nullish(),
password: z.string().min(8).max(2048).optional(),
newPassword: z.string().min(8).max(2048).optional(),
oldPassword: z.string().min(8).max(2048).optional(),
archiveAsScreenshot: z.boolean().optional(),
archiveAsPDF: z.boolean().optional(),
archiveAsMonolith: z.boolean().optional(),
archiveAsWaybackMachine: z.boolean().optional(),
locale: z.string().max(20).optional(),
isPrivate: z.boolean().optional(),
preventDuplicateLinks: z.boolean().optional(),
collectionOrder: z.array(z.number()).optional(),
linksRouteTo: z.nativeEnum(LinksRouteTo).optional(),
whitelistedUsers: z.array(z.string().max(50)).optional(),
referredBy: z.string().max(100).nullish(),
});
};
export const PostSessionSchema = z.object({
username: z.string().min(3).max(50),
password: z.string().min(8),
sessionName: z.string().trim().max(50).optional(),
});
export const PostLinkSchema = z.object({
type: z.enum(["url", "pdf", "image"]),
url: z.string().trim().max(2048).url().optional(),
name: z.string().trim().max(2048).optional(),
description: z.string().trim().max(2048).optional(),
collection: z
.object({
id: z.number().optional(),
name: z.string().trim().max(2048).optional(),
})
.optional(),
tags:
z
.array(
z.object({
id: z.number().optional(),
name: z.string().trim().max(50),
})
)
.optional() || [],
});
export type PostLinkSchemaType = z.infer<typeof PostLinkSchema>;
export const UpdateLinkSchema = z.object({
id: z.number(),
name: z.string().trim().max(2048).optional(),
url: z.string().trim().max(2048).optional(),
description: z.string().trim().max(2048).optional(),
icon: z.string().trim().max(50).nullish(),
iconWeight: z.string().trim().max(50).nullish(),
color: z.string().trim().max(10).nullish(),
collection: z.object({
id: z.number(),
ownerId: z.number(),
}),
tags: z.array(
z.object({
id: z.number().optional(),
name: z.string().trim().max(50),
})
),
pinnedBy: z
.array(
z
.object({
id: z.number().optional(),
})
.optional()
)
.optional(),
});
export type UpdateLinkSchemaType = z.infer<typeof UpdateLinkSchema>;
const ACCEPTED_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"application/pdf",
];
const NEXT_PUBLIC_MAX_FILE_BUFFER = Number(
process.env.NEXT_PUBLIC_MAX_FILE_BUFFER || 10
);
const MAX_FILE_SIZE = NEXT_PUBLIC_MAX_FILE_BUFFER * 1024 * 1024;
export const UploadFileSchema = z.object({
file: z
.any()
.refine((files) => files?.length == 1, "File is required.")
.refine(
(files) => files?.[0]?.size <= MAX_FILE_SIZE,
`Max file size is ${MAX_FILE_SIZE}MB.`
)
.refine(
(files) => ACCEPTED_TYPES.includes(files?.[0]?.mimetype),
`Only ${ACCEPTED_TYPES.join(", ")} files are accepted.`
),
id: z.number(),
format: z.nativeEnum(ArchivedFormat),
});
export const PostCollectionSchema = z.object({
name: z.string().trim().max(2048),
description: z.string().trim().max(2048).optional(),
color: z.string().trim().max(10).optional(),
icon: z.string().trim().max(50).optional(),
iconWeight: z.string().trim().max(50).optional(),
parentId: z.number().optional(),
});
export type PostCollectionSchemaType = z.infer<typeof PostCollectionSchema>;
export const UpdateCollectionSchema = z.object({
id: z.number(),
name: z.string().trim().max(2048),
description: z.string().trim().max(2048).optional(),
color: z.string().trim().max(10).optional(),
isPublic: z.boolean().optional(),
icon: z.string().trim().max(50).nullish(),
iconWeight: z.string().trim().max(50).nullish(),
parentId: z.union([z.number(), z.literal("root")]).nullish(),
members: z.array(
z.object({
userId: z.number(),
canCreate: z.boolean(),
canUpdate: z.boolean(),
canDelete: z.boolean(),
})
),
});
export type UpdateCollectionSchemaType = z.infer<typeof UpdateCollectionSchema>;
export const UpdateTagSchema = z.object({
name: z.string().trim().max(50),
});
export type UpdateTagSchemaType = z.infer<typeof UpdateTagSchema>;

View File

@@ -1,6 +1,6 @@
{
"name": "linkwarden",
"version": "v2.7.1",
"version": "v2.8.0",
"main": "index.js",
"repository": "https://github.com/linkwarden/linkwarden.git",
"author": "Daniel31X13 <daniel31x13@gmail.com>",
@@ -25,7 +25,9 @@
"@aws-sdk/client-s3": "^3.379.1",
"@headlessui/react": "^1.7.15",
"@mozilla/readability": "^0.4.4",
"@prisma/client": "^4.16.2",
"@phosphor-icons/core": "^2.1.1",
"@phosphor-icons/react": "^2.1.7",
"@prisma/client": "^5.21.1",
"@stripe/stripe-js": "^1.54.1",
"@tanstack/react-query": "^5.51.15",
"@tanstack/react-query-devtools": "^5.51.15",
@@ -50,6 +52,7 @@
"eslint-config-next": "13.4.9",
"formidable": "^3.5.1",
"framer-motion": "^10.16.4",
"fuse.js": "^7.0.0",
"handlebars": "^4.7.8",
"himalaya": "^1.1.0",
"i18next": "^23.11.5",
@@ -73,10 +76,12 @@
"react-masonry-css": "^1.0.16",
"react-select": "^5.7.4",
"react-spinners": "^0.14.1",
"react-window": "^1.8.10",
"socks-proxy-agent": "^8.0.2",
"stripe": "^12.13.0",
"tailwind-merge": "^2.3.0",
"vaul": "^0.8.8",
"vaul": "^1.1.1",
"zod": "^3.23.8",
"zustand": "^4.3.8"
},
"devDependencies": {
@@ -85,14 +90,15 @@
"@types/dompurify": "^3.0.4",
"@types/jsdom": "^21.1.3",
"@types/node-fetch": "^2.6.10",
"@types/react-window": "^1.8.8",
"@types/shelljs": "^0.8.15",
"autoprefixer": "^10.4.14",
"daisyui": "^4.4.2",
"nodemon": "^3.0.2",
"postcss": "^8.4.26",
"prettier": "3.1.1",
"prisma": "^4.16.2",
"tailwindcss": "^3.3.3",
"prisma": "^5.21.1",
"tailwindcss": "^3.4.10",
"ts-node": "^10.9.2",
"typescript": "4.9.4"
}

View File

@@ -0,0 +1,42 @@
import getTags from "@/lib/api/controllers/tags/getTags";
import { prisma } from "@/lib/api/db";
import { LinkRequestQuery } from "@/types/global";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function collections(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "GET") {
// Convert the type of the request query to "LinkRequestQuery"
const convertedData: Omit<LinkRequestQuery, "tagId"> = {
sort: Number(req.query.sort as string),
collectionId: req.query.collectionId
? Number(req.query.collectionId as string)
: undefined,
};
if (!convertedData.collectionId) {
return res
.status(400)
.json({ response: "Please choose a valid collection." });
}
const collection = await prisma.collection.findFirst({
where: {
id: convertedData.collectionId,
isPublic: true,
},
});
if (!collection) {
return res.status(404).json({ response: "Collection not found." });
}
const tags = await getTags({
collectionId: collection.id,
});
return res.status(tags?.status || 500).json({ response: tags?.response });
}
}

View File

@@ -7,7 +7,9 @@ export default async function tags(req: NextApiRequest, res: NextApiResponse) {
if (!user) return;
if (req.method === "GET") {
const tags = await getTags(user.id);
return res.status(tags.status).json({ response: tags.response });
const tags = await getTags({
userId: user.id,
});
return res.status(tags?.status || 500).json({ response: tags?.response });
}
}

View File

@@ -135,6 +135,21 @@ export default function Index() {
<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 mt-1">
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
for (const link of links) {
if (link.url) window.open(link.url, "_blank");
}
}}
className="whitespace-nowrap"
>
{t("open_all_links")}
</div>
</li>
{permissions === true && (
<li>
<div

View File

@@ -1,7 +1,6 @@
import MainLayout from "@/layouts/MainLayout";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import useWindowDimensions from "@/hooks/useWindowDimensions";
import React from "react";
import { toast } from "react-hot-toast";
import { MigrationFormat, MigrationRequest, ViewMode } from "@/types/global";
@@ -16,16 +15,23 @@ import { useCollections } from "@/hooks/store/collections";
import { useTags } from "@/hooks/store/tags";
import { useDashboardData } from "@/hooks/store/dashboardData";
import Links from "@/components/LinkViews/Links";
import useLocalSettingsStore from "@/store/localSettings";
import { useUpdateUser, useUser } from "@/hooks/store/user";
import SurveyModal from "@/components/ModalContent/SurveyModal";
export default function Dashboard() {
const { t } = useTranslation();
const { data: collections = [] } = useCollections();
const dashboardData = useDashboardData();
const {
data: { links = [], numberOfPinnedLinks } = { links: [] },
...dashboardData
} = useDashboardData();
const { data: tags = [] } = useTags();
const { data: account = [] } = useUser();
const [numberOfLinks, setNumberOfLinks] = useState(0);
const [showLinks, setShowLinks] = useState(3);
const { settings } = useLocalSettingsStore();
useEffect(() => {
setNumberOfLinks(
@@ -37,29 +43,44 @@ export default function Dashboard() {
);
}, [collections]);
const handleNumberOfLinksToShow = () => {
if (window.innerWidth > 1900) {
setShowLinks(10);
} else if (window.innerWidth > 1500) {
setShowLinks(8);
} else if (window.innerWidth > 880) {
setShowLinks(6);
} else if (window.innerWidth > 550) {
setShowLinks(4);
} else setShowLinks(2);
};
const { width } = useWindowDimensions();
useEffect(() => {
handleNumberOfLinksToShow();
}, [width]);
if (
process.env.NEXT_PUBLIC_STRIPE === "true" &&
account &&
account.id &&
account.referredBy === null &&
// if user is using Linkwarden for more than 3 days
new Date().getTime() - new Date(account.createdAt).getTime() >
3 * 24 * 60 * 60 * 1000
) {
setTimeout(() => {
setShowsSurveyModal(true);
}, 1000);
}
}, [account]);
const importBookmarks = async (e: any, format: MigrationFormat) => {
const file: File = e.target.files[0];
const numberOfLinksToShow = useMemo(() => {
if (window.innerWidth > 1900) {
return 10;
} else if (window.innerWidth > 1500) {
return 8;
} else if (window.innerWidth > 880) {
return 6;
} else if (window.innerWidth > 550) {
return 4;
} else {
return 2;
}
}, []);
const importBookmarks = async (
e: React.ChangeEvent<HTMLInputElement>,
format: MigrationFormat
) => {
const file: File | null = e.target.files && e.target.files[0];
if (file) {
var reader = new FileReader();
const reader = new FileReader();
reader.readAsText(file, "UTF-8");
reader.onload = async function (e) {
const load = toast.loading("Importing...");
@@ -71,23 +92,44 @@ export default function Dashboard() {
data: request,
};
const response = await fetch("/api/v1/migration", {
method: "POST",
body: JSON.stringify(body),
});
try {
const response = await fetch("/api/v1/migration", {
method: "POST",
body: JSON.stringify(body),
});
await response.json();
if (!response.ok) {
const errorData = await response.json();
toast.dismiss(load);
toast.dismiss(load);
toast.error(
errorData.response ||
"Failed to import bookmarks. Please try again."
);
return;
}
toast.success("Imported the Bookmarks! Reloading the page...");
await response.json();
toast.dismiss(load);
toast.success("Imported the Bookmarks! Reloading the page...");
setTimeout(() => {
location.reload();
}, 2000);
setTimeout(() => {
location.reload();
}, 2000);
} catch (error) {
console.error("Request failed", error);
toast.dismiss(load);
toast.error(
"An error occurred while importing bookmarks. Please check the logs for more info."
);
}
};
reader.onerror = function (e) {
console.log("Error:", e);
console.log("Error reading file:", e);
toast.error(
"Failed to read the file. Please make sure the file is correct and try again."
);
};
}
};
@@ -98,6 +140,42 @@ export default function Dashboard() {
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
);
const [showSurveyModal, setShowsSurveyModal] = useState(false);
const { data: user } = useUser();
const updateUser = useUpdateUser();
const [submitLoader, setSubmitLoader] = useState(false);
const submitSurvey = async (referer: string, other?: string) => {
if (submitLoader) return;
setSubmitLoader(true);
const load = toast.loading(t("applying"));
await updateUser.mutateAsync(
{
...user,
referredBy: referer === "other" ? "Other: " + other : referer,
},
{
onSettled: (data, error) => {
console.log(data, error);
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
toast.success(t("thanks_for_feedback"));
setShowsSurveyModal(false);
}
},
}
);
};
return (
<MainLayout>
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
@@ -110,32 +188,30 @@ export default function Dashboard() {
<ViewDropdown viewMode={viewMode} setViewMode={setViewMode} />
</div>
<div>
<div className="flex justify-evenly flex-col xl:flex-row xl:items-center gap-2 xl:w-full h-full rounded-2xl p-8 border border-neutral-content bg-base-200">
<DashboardItem
name={numberOfLinks === 1 ? t("link") : t("links")}
value={numberOfLinks}
icon={"bi-link-45deg"}
/>
<div className="xl:flex flex flex-col sm:grid grid-cols-2 gap-3 xl:flex-row xl:justify-evenly xl:w-full h-full">
<DashboardItem
name={numberOfLinks === 1 ? t("link") : t("links")}
value={numberOfLinks}
icon={"bi-link-45deg"}
/>
<div className="divider xl:divider-horizontal"></div>
<DashboardItem
name={collections.length === 1 ? t("collection") : t("collections")}
value={collections.length}
icon={"bi-folder"}
/>
<DashboardItem
name={
collections.length === 1 ? t("collection") : t("collections")
}
value={collections.length}
icon={"bi-folder"}
/>
<DashboardItem
name={tags.length === 1 ? t("tag") : t("tags")}
value={tags.length}
icon={"bi-hash"}
/>
<div className="divider xl:divider-horizontal"></div>
<DashboardItem
name={tags.length === 1 ? t("tag") : t("tags")}
value={tags.length}
icon={"bi-hash"}
/>
</div>
<DashboardItem
name={t("pinned")}
value={numberOfPinnedLinks}
icon={"bi-pin-angle"}
/>
</div>
<div className="flex justify-between items-center">
@@ -157,10 +233,7 @@ export default function Dashboard() {
<div
style={{
flex:
dashboardData.data || dashboardData.isLoading
? "0 1 auto"
: "1 1 auto",
flex: links || dashboardData.isLoading ? "0 1 auto" : "1 1 auto",
}}
className="flex flex-col 2xl:flex-row items-start 2xl:gap-2"
>
@@ -168,21 +241,22 @@ export default function Dashboard() {
<div className="w-full">
<Links
layout={viewMode}
placeholderCount={showLinks / 2}
placeholderCount={settings.columns || 1}
useData={dashboardData}
/>
</div>
) : dashboardData.data &&
dashboardData.data[0] &&
!dashboardData.isLoading ? (
) : links && links[0] && !dashboardData.isLoading ? (
<div className="w-full">
<Links
links={dashboardData.data.slice(0, showLinks)}
links={links.slice(
0,
settings.columns ? settings.columns * 2 : numberOfLinksToShow
)}
layout={viewMode}
/>
</div>
) : (
<div className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200">
<div className="flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200 bg-gradient-to-tr from-neutral-content/70 to-50% to-base-200">
<p className="text-center text-2xl">
{t("view_added_links_here")}
</p>
@@ -310,23 +384,28 @@ export default function Dashboard() {
<div className="w-full">
<Links
layout={viewMode}
placeholderCount={showLinks / 2}
placeholderCount={settings.columns || 1}
useData={dashboardData}
/>
</div>
) : dashboardData.data?.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
) : links?.some((e: any) => e.pinnedBy && e.pinnedBy[0]) ? (
<div className="w-full">
<Links
links={dashboardData.data
.filter((e) => e.pinnedBy && e.pinnedBy[0])
.slice(0, showLinks)}
links={links
.filter((e: any) => e.pinnedBy && e.pinnedBy[0])
.slice(
0,
settings.columns
? settings.columns * 2
: numberOfLinksToShow
)}
layout={viewMode}
/>
</div>
) : (
<div
style={{ flex: "1 1 auto" }}
className="flex flex-col gap-2 justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
className="flex flex-col gap-2 justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200 bg-gradient-to-tr from-neutral-content/70 to-50% to-base-200"
>
<i className="bi-pin mx-auto text-6xl text-primary"></i>
<p className="text-center text-2xl">
@@ -339,9 +418,15 @@ export default function Dashboard() {
)}
</div>
</div>
{newLinkModal ? (
<NewLinkModal onClose={() => setNewLinkModal(false)} />
) : undefined}
{showSurveyModal && (
<SurveyModal
submit={submitSurvey}
onClose={() => {
setShowsSurveyModal(false);
}}
/>
)}
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
</MainLayout>
);
}

View File

@@ -1,70 +1,70 @@
import React, { useEffect, useState } from "react";
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import { ArchivedFormat } from "@/types/global";
import ReadableView from "@/components/ReadableView";
import getServerSideProps from "@/lib/client/getServerSideProps";
import { useGetLink, useLinks } from "@/hooks/store/links";
import { useGetLink } from "@/hooks/store/links";
import clsx from "clsx";
export default function Index() {
const { links } = useLinks();
const getLink = useGetLink();
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
const router = useRouter();
useEffect(() => {
const fetchLink = async () => {
if (router.query.id) {
await getLink.mutateAsync(Number(router.query.id));
}
};
fetchLink();
if (router.query.id) {
getLink.mutateAsync({ id: Number(router.query.id) });
}
}, []);
useEffect(() => {
if (links && links[0])
setLink(links.find((e) => e.id === Number(router.query.id)));
}, [links]);
return (
<div className="relative">
<div className={clsx(getLink.isPending ? "flex h-screen" : "relative")}>
{/* <div className="fixed left-1/2 transform -translate-x-1/2 w-fit py-1 px-3 bg-base-200 border border-neutral-content rounded-md">
Readable
</div> */}
{link && Number(router.query.format) === ArchivedFormat.readability && (
<ReadableView link={link} />
)}
{link && Number(router.query.format) === ArchivedFormat.monolith && (
{getLink.data?.id &&
Number(router.query.format) === ArchivedFormat.readability ? (
<ReadableView link={getLink.data} />
) : getLink.data?.id &&
Number(router.query.format) === ArchivedFormat.monolith ? (
<iframe
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.monolith}`}
src={`/api/v1/archives/${getLink.data.id}?format=${ArchivedFormat.monolith}`}
className="w-full h-screen border-none"
></iframe>
)}
{link && Number(router.query.format) === ArchivedFormat.pdf && (
) : getLink.data?.id &&
Number(router.query.format) === ArchivedFormat.pdf ? (
<iframe
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.pdf}`}
src={`/api/v1/archives/${getLink.data.id}?format=${ArchivedFormat.pdf}`}
className="w-full h-screen border-none"
></iframe>
)}
{link && Number(router.query.format) === ArchivedFormat.png && (
) : getLink.data?.id &&
Number(router.query.format) === ArchivedFormat.png ? (
<img
alt=""
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.png}`}
src={`/api/v1/archives/${getLink.data.id}?format=${ArchivedFormat.png}`}
className="w-fit mx-auto"
/>
)}
{link && Number(router.query.format) === ArchivedFormat.jpeg && (
) : getLink.data?.id &&
Number(router.query.format) === ArchivedFormat.jpeg ? (
<img
alt=""
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}`}
src={`/api/v1/archives/${getLink.data.id}?format=${ArchivedFormat.jpeg}`}
className="w-fit mx-auto"
/>
) : getLink.error ? (
<p>404 - Not found</p>
) : (
<div className="max-w-3xl p-5 m-auto w-full flex flex-col items-center gap-5">
<div className="w-full mr-auto h-4 skeleton rounded-md"></div>
<div className="w-full mr-auto h-4 skeleton rounded-md"></div>
<div className="w-full mr-auto h-4 skeleton rounded-md"></div>
<div className="w-3/4 mr-auto h-4 skeleton rounded-md"></div>
<div className="w-5/6 mr-auto h-4 skeleton rounded-md"></div>
<div className="w-3/4 mr-auto h-4 skeleton rounded-md"></div>
<div className="w-full mr-auto h-4 skeleton rounded-md"></div>
<div className="w-full mr-auto h-4 skeleton rounded-md"></div>
<div className="w-5/6 mr-auto h-4 skeleton rounded-md"></div>
</div>
)}
</div>
);

View File

@@ -1,6 +1,7 @@
"use client";
import getPublicCollectionData from "@/lib/client/getPublicCollectionData";
import {
AccountSettings,
CollectionIncludingMembersAndLinkCount,
Sort,
ViewMode,
@@ -21,6 +22,7 @@ import getServerSideProps from "@/lib/client/getServerSideProps";
import LinkListOptions from "@/components/LinkListOptions";
import { usePublicLinks } from "@/hooks/store/publicLinks";
import Links from "@/components/LinkViews/Links";
import { usePublicTags } from "@/hooks/store/publicTags";
export default function PublicCollections() {
const { t } = useTranslation();
@@ -29,15 +31,35 @@ export default function PublicCollections() {
const router = useRouter();
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
const handleTagSelection = (tag: string | undefined) => {
if (tag) {
Object.keys(searchFilter).forEach(
(v) =>
(searchFilter[
v as keyof {
name: boolean;
url: boolean;
description: boolean;
tags: boolean;
textContent: boolean;
}
] = false)
);
searchFilter.tags = true;
return router.push(
"/public/collections/" +
router.query.id +
"?q=" +
encodeURIComponent(tag || "")
);
} else {
return router.push("/public/collections/" + router.query.id);
}
};
const [searchFilter, setSearchFilter] = useState({
name: true,
@@ -51,6 +73,8 @@ export default function PublicCollections() {
Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst
);
const { data: tags } = usePublicTags();
const { links, data } = usePublicLinks({
sort: sortBy,
searchQueryString: router.query.q
@@ -62,10 +86,8 @@ export default function PublicCollections() {
searchByTextContent: searchFilter.textContent,
searchByTags: searchFilter.tags,
});
const [collection, setCollection] =
useState<CollectionIncludingMembersAndLinkCount>();
useEffect(() => {
if (router.query.id) {
getPublicCollectionData(Number(router.query.id)).then((res) => {
@@ -93,160 +115,204 @@ export default function PublicCollections() {
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
);
return collection ? (
<div
className="h-96"
style={{
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
settings.theme === "dark" ? "#262626" : "#f3f4f6"
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}}
>
{collection ? (
<Head>
<title>{collection.name} | Linkwarden</title>
<meta
property="og:title"
content={`${collection.name} | Linkwarden`}
key="title"
/>
</Head>
) : undefined}
<div className="lg:w-3/4 w-full mx-auto p-5 bg">
<div className="flex items-center justify-between">
<p className="text-4xl font-thin mb-2 capitalize mt-10">
{collection.name}
</p>
<div className="flex gap-2 items-center mt-8 min-w-fit">
<ToggleDarkMode />
if (!collection) return <></>;
else
return (
<div
className="h-96"
style={{
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
settings.theme === "dark" ? "#262626" : "#f3f4f6"
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}}
>
{collection && (
<Head>
<title>{collection.name} | Linkwarden</title>
<meta
property="og:title"
content={`${collection.name} | Linkwarden`}
key="title"
/>
</Head>
)}
<div className="lg:w-3/4 w-full mx-auto p-5 bg">
<div className="flex items-center justify-between">
<p className="text-4xl font-thin mb-2 capitalize mt-10">
{collection.name}
</p>
<div className="flex gap-2 items-center mt-8 min-w-fit">
<ToggleDarkMode />
<Link href="https://linkwarden.app/" target="_blank">
<Image
src={`/icon.png`}
width={551}
height={551}
alt="Linkwarden"
title={t("list_created_with_linkwarden")}
className="h-8 w-fit mx-auto rounded"
/>
</Link>
<Link href="https://linkwarden.app/" target="_blank">
<Image
src={`/icon.png`}
width={551}
height={551}
alt="Linkwarden"
title={t("list_created_with_linkwarden")}
className="h-8 w-fit mx-auto rounded"
/>
</Link>
</div>
</div>
</div>
<div className="mt-3">
<div className={`min-w-[15rem]`}>
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
<div
className="flex items-center btn px-2 btn-ghost rounded-full"
onClick={() => setEditCollectionSharingModal(true)}
>
{collectionOwner.id ? (
<ProfilePhoto
src={collectionOwner.image || undefined}
name={collectionOwner.name}
/>
) : undefined}
{collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => {
return (
<ProfilePhoto
key={i}
src={e.user.image ? e.user.image : undefined}
className="-ml-3"
name={e.user.name}
/>
);
})
.slice(0, 3)}
{collection.members.length - 3 > 0 ? (
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
<span>+{collection.members.length - 3}</span>
</div>
</div>
) : null}
</div>
<p className="text-neutral text-sm">
{collection.members.length > 0 &&
collection.members.length === 1
? t("by_author_and_other", {
author: collectionOwner.name,
count: collection.members.length,
<div className="mt-3">
<div className={`min-w-[15rem]`}>
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
<div
className="flex items-center btn px-2 btn-ghost rounded-full"
onClick={() => setEditCollectionSharingModal(true)}
>
{collectionOwner.id && (
<ProfilePhoto
src={collectionOwner.image || undefined}
name={collectionOwner.name}
/>
)}
{collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => {
return (
<ProfilePhoto
key={i}
src={e.user.image ? e.user.image : undefined}
className="-ml-3"
name={e.user.name}
/>
);
})
: collection.members.length > 0 &&
collection.members.length !== 1
? t("by_author_and_others", {
.slice(0, 3)}
{collection.members.length - 3 > 0 && (
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
<span>+{collection.members.length - 3}</span>
</div>
</div>
)}
</div>
<p className="text-neutral text-sm">
{collection.members.length > 0 &&
collection.members.length === 1
? t("by_author_and_other", {
author: collectionOwner.name,
count: collection.members.length,
})
: t("by_author", {
author: collectionOwner.name,
})}
</p>
: collection.members.length > 0 &&
collection.members.length !== 1
? t("by_author_and_others", {
author: collectionOwner.name,
count: collection.members.length,
})
: t("by_author", {
author: collectionOwner.name,
})}
</p>
</div>
</div>
</div>
</div>
<p className="mt-5">{collection.description}</p>
<p className="mt-5">{collection.description}</p>
<div className="divider mt-5 mb-0"></div>
<div className="divider mt-5 mb-0"></div>
<div className="flex mb-5 mt-10 flex-col gap-5">
<LinkListOptions
t={t}
viewMode={viewMode}
setViewMode={setViewMode}
sortBy={sortBy}
setSortBy={setSortBy}
searchFilter={searchFilter}
setSearchFilter={setSearchFilter}
>
<SearchBar
placeholder={
collection._count?.links === 1
? t("search_count_link", {
count: collection._count?.links,
})
: t("search_count_links", {
count: collection._count?.links,
})
<div className="flex mb-5 mt-10 flex-col gap-5">
<LinkListOptions
t={t}
viewMode={viewMode}
setViewMode={setViewMode}
sortBy={sortBy}
setSortBy={setSortBy}
searchFilter={searchFilter}
setSearchFilter={setSearchFilter}
>
<SearchBar
placeholder={
collection._count?.links === 1
? t("search_count_link", {
count: collection._count?.links,
})
: t("search_count_links", {
count: collection._count?.links,
})
}
/>
</LinkListOptions>
{tags && tags[0] && (
<div className="flex gap-2 mt-2 mb-6 flex-wrap">
<button
className="max-w-full"
onClick={() => handleTagSelection(undefined)}
>
<div
className={`${
!router.query.q
? "bg-primary/20"
: "bg-neutral-content/20 hover:bg-neutral/20"
} duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 rounded-md h-8`}
>
<p className="truncate px-3">{t("all_links")}</p>
</div>
</button>
{tags
.map((t) => t.name)
.filter((item, pos, self) => self.indexOf(item) === pos)
.sort((a, b) => a.localeCompare(b))
.map((e, i) => {
const active = router.query.q === e;
return (
<button
className="max-w-full"
key={i}
onClick={() => handleTagSelection(e)}
>
<div
className={`${
active
? "bg-primary/20"
: "bg-neutral-content/20 hover:bg-neutral/20"
} duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 rounded-md h-8`}
>
<i className="bi-hash text-2xl text-primary drop-shadow"></i>
<p className="truncate pr-3">{e}</p>
</div>
</button>
);
})}
</div>
)}
<Links
links={
links?.map((e, i) => {
const linkWithCollectionData = {
...e,
collection: collection, // Append collection data
};
return linkWithCollectionData;
}) as any
}
layout={viewMode}
placeholderCount={1}
useData={data}
/>
</LinkListOptions>
{!data.isLoading && links && !links[0] && (
<p>{t("nothing_found")}</p>
)}
<Links
links={
links?.map((e, i) => {
const linkWithCollectionData = {
...e,
collection: collection, // Append collection data
};
return linkWithCollectionData;
}) as any
}
layout={viewMode}
placeholderCount={1}
useData={data}
/>
{!data.isLoading && links && !links[0] && <p>{t("nothing_found")}</p>}
{/* <p className="text-center text-neutral">
{/* <p className="text-center text-neutral">
List created with <span className="text-black">Linkwarden.</span>
</p> */}
</div>
</div>
{editCollectionSharingModal && (
<EditCollectionSharingModal
onClose={() => setEditCollectionSharingModal(false)}
activeCollection={collection}
/>
)}
</div>
{editCollectionSharingModal ? (
<EditCollectionSharingModal
onClose={() => setEditCollectionSharingModal(false)}
activeCollection={collection}
/>
) : undefined}
</div>
) : (
<></>
);
);
}
export { getServerSideProps };

View File

@@ -1,63 +1,70 @@
import React, { useEffect, useState } from "react";
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import { ArchivedFormat } from "@/types/global";
import ReadableView from "@/components/ReadableView";
import getServerSideProps from "@/lib/client/getServerSideProps";
import { useGetLink, useLinks } from "@/hooks/store/links";
import { useGetLink } from "@/hooks/store/links";
import clsx from "clsx";
export default function Index() {
const { links } = useLinks();
const getLink = useGetLink();
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
const router = useRouter();
useEffect(() => {
const fetchLink = async () => {
if (router.query.id) {
await getLink.mutateAsync(Number(router.query.id));
}
};
fetchLink();
if (router.query.id) {
getLink.mutateAsync({ id: Number(router.query.id), isPublicRoute: true });
}
}, []);
useEffect(() => {
if (links && links[0])
setLink(links.find((e) => e.id === Number(router.query.id)));
}, [links]);
return (
<div className="relative">
<div className={clsx(getLink.isPending ? "flex h-screen" : "relative")}>
{/* <div className="fixed left-1/2 transform -translate-x-1/2 w-fit py-1 px-3 bg-base-200 border border-neutral-content rounded-md">
Readable
</div> */}
{link && Number(router.query.format) === ArchivedFormat.readability && (
<ReadableView link={link} />
)}
{link && Number(router.query.format) === ArchivedFormat.pdf && (
{getLink.data?.id &&
Number(router.query.format) === ArchivedFormat.readability ? (
<ReadableView link={getLink.data} />
) : getLink.data?.id &&
Number(router.query.format) === ArchivedFormat.monolith ? (
<iframe
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.pdf}`}
src={`/api/v1/archives/${getLink.data.id}?format=${ArchivedFormat.monolith}`}
className="w-full h-screen border-none"
></iframe>
)}
{link && Number(router.query.format) === ArchivedFormat.png && (
) : getLink.data?.id &&
Number(router.query.format) === ArchivedFormat.pdf ? (
<iframe
src={`/api/v1/archives/${getLink.data.id}?format=${ArchivedFormat.pdf}`}
className="w-full h-screen border-none"
></iframe>
) : getLink.data?.id &&
Number(router.query.format) === ArchivedFormat.png ? (
<img
alt=""
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.png}`}
src={`/api/v1/archives/${getLink.data.id}?format=${ArchivedFormat.png}`}
className="w-fit mx-auto"
/>
)}
{link && Number(router.query.format) === ArchivedFormat.jpeg && (
) : getLink.data?.id &&
Number(router.query.format) === ArchivedFormat.jpeg ? (
<img
alt=""
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}`}
src={`/api/v1/archives/${getLink.data.id}?format=${ArchivedFormat.jpeg}`}
className="w-fit mx-auto"
/>
) : getLink.error ? (
<p>404 - Not found</p>
) : (
<div className="max-w-3xl p-5 m-auto w-full flex flex-col items-center gap-5">
<div className="w-full mr-auto h-4 skeleton rounded-md"></div>
<div className="w-full mr-auto h-4 skeleton rounded-md"></div>
<div className="w-full mr-auto h-4 skeleton rounded-md"></div>
<div className="w-3/4 mr-auto h-4 skeleton rounded-md"></div>
<div className="w-5/6 mr-auto h-4 skeleton rounded-md"></div>
<div className="w-3/4 mr-auto h-4 skeleton rounded-md"></div>
<div className="w-full mr-auto h-4 skeleton rounded-md"></div>
<div className="w-full mr-auto h-4 skeleton rounded-md"></div>
<div className="w-5/6 mr-auto h-4 skeleton rounded-md"></div>
</div>
)}
</div>
);

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, ChangeEvent } from "react";
import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast";
import SettingsLayout from "@/layouts/SettingsLayout";
@@ -17,6 +17,7 @@ import { i18n } from "next-i18next.config";
import { useTranslation } from "next-i18next";
import getServerSideProps from "@/lib/client/getServerSideProps";
import { useUpdateUser, useUser } from "@/hooks/store/user";
import { z } from "zod";
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
@@ -55,8 +56,10 @@ export default function Account() {
if (!objectIsEmpty(account)) setUser({ ...account });
}, [account]);
const handleImageUpload = async (e: any) => {
const file: File = e.target.files[0];
const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return toast.error(t("image_upload_no_file_error"));
const fileExtension = file.name.split(".").pop()?.toLowerCase();
const allowedExtensions = ["png", "jpeg", "jpg"];
if (allowedExtensions.includes(fileExtension as string)) {
@@ -78,6 +81,16 @@ export default function Account() {
};
const submit = async (password?: string) => {
if (!/^[a-z0-9_-]{3,50}$/.test(user.username || "")) {
return toast.error(t("username_invalid_guide"));
}
const emailSchema = z.string().trim().email().toLowerCase();
const emailValidation = emailSchema.safeParse(user.email || "");
if (!emailValidation.success) {
return toast.error(t("email_invalid"));
}
setSubmitLoader(true);
const load = toast.loading(t("applying_settings"));
@@ -88,13 +101,8 @@ export default function Account() {
password: password ? password : undefined,
},
{
onSuccess: (data) => {
if (data.response.email !== user.email) {
toast.success(t("email_change_request"));
setEmailChangeVerificationModal(false);
}
},
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -111,39 +119,72 @@ export default function Account() {
}
);
setSubmitLoader(false);
if (user.locale !== account.locale) {
setTimeout(() => {
location.reload();
}, 1000);
}
};
const importBookmarks = async (e: any, format: MigrationFormat) => {
setSubmitLoader(true);
const file: File = e.target.files[0];
const importBookmarks = async (
e: React.ChangeEvent<HTMLInputElement>,
format: MigrationFormat
) => {
const file: File | null = e.target.files && e.target.files[0];
if (file) {
var reader = new FileReader();
const reader = new FileReader();
reader.readAsText(file, "UTF-8");
reader.onload = async function (e) {
const load = toast.loading(t("importing_bookmarks"));
const load = toast.loading("Importing...");
const request: string = e.target?.result as string;
const body: MigrationRequest = { format, data: request };
const response = await fetch("/api/v1/migration", {
method: "POST",
body: JSON.stringify(body),
});
const data = await response.json();
toast.dismiss(load);
if (response.ok) {
toast.success(t("import_success"));
const body: MigrationRequest = {
format,
data: request,
};
try {
const response = await fetch("/api/v1/migration", {
method: "POST",
body: JSON.stringify(body),
});
if (!response.ok) {
const errorData = await response.json();
toast.dismiss(load);
toast.error(
errorData.response ||
"Failed to import bookmarks. Please try again."
);
return;
}
await response.json();
toast.dismiss(load);
toast.success("Imported the Bookmarks! Reloading the page...");
setTimeout(() => {
location.reload();
}, 2000);
} else {
toast.error(data.response as string);
} catch (error) {
console.error("Request failed", error);
toast.dismiss(load);
toast.error(
"An error occurred while importing bookmarks. Please check the logs for more info."
);
}
};
reader.onerror = function (e) {
console.log("Error:", e);
console.log("Error reading file:", e);
toast.error(
"Failed to read the file. Please make sure the file is correct and try again."
);
};
}
setSubmitLoader(false);
};
const [whitelistedUsersTextbox, setWhiteListedUsersTextbox] = useState("");
@@ -190,16 +231,17 @@ export default function Account() {
onChange={(e) => setUser({ ...user, username: e.target.value })}
/>
</div>
{emailEnabled ? (
{emailEnabled && (
<div>
<p className="mb-2">{t("email")}</p>
<TextInput
value={user.email || ""}
type="email"
className="bg-base-200"
onChange={(e) => setUser({ ...user, email: e.target.value })}
/>
</div>
) : undefined}
)}
<div>
<p className="mb-2">{t("language")}</p>
<select
@@ -437,9 +479,8 @@ export default function Account() {
<p>
{t("delete_account_warning")}
{process.env.NEXT_PUBLIC_STRIPE
? " " + t("cancel_subscription_notice")
: undefined}
{process.env.NEXT_PUBLIC_STRIPE &&
" " + t("cancel_subscription_notice")}
</p>
</div>
@@ -448,14 +489,14 @@ export default function Account() {
</Link>
</div>
{emailChangeVerificationModal ? (
{emailChangeVerificationModal && (
<EmailChangeVerificationModal
onClose={() => setEmailChangeVerificationModal(false)}
onSubmit={submit}
oldEmail={account.email || ""}
newEmail={user.email || ""}
/>
) : undefined}
)}
</SettingsLayout>
);
}

View File

@@ -1,17 +1,57 @@
import SettingsLayout from "@/layouts/SettingsLayout";
import { useRouter } from "next/router";
import { useEffect } from "react";
import InviteModal from "@/components/ModalContent/InviteModal";
import { User as U } from "@prisma/client";
import { useEffect, useState } from "react";
import { useTranslation } from "next-i18next";
import getServerSideProps from "@/lib/client/getServerSideProps";
import { useUsers } from "@/hooks/store/admin/users";
import DeleteUserModal from "@/components/ModalContent/DeleteUserModal";
import { useUser } from "@/hooks/store/user";
import { dropdownTriggerer } from "@/lib/client/utils";
import clsx from "clsx";
import { signIn } from "next-auth/react";
import toast from "react-hot-toast";
interface User extends U {
subscriptions: {
active: boolean;
};
}
type UserModal = {
isOpen: boolean;
userId: number | null;
};
export default function Billing() {
const router = useRouter();
const { t } = useTranslation();
const { data: account } = useUser();
const { data: users = [] } = useUsers();
useEffect(() => {
if (!process.env.NEXT_PUBLIC_STRIPE) router.push("/settings/profile");
if (!process.env.NEXT_PUBLIC_STRIPE || account.parentSubscriptionId)
router.push("/settings/account");
}, []);
const [searchQuery, setSearchQuery] = useState("");
const [filteredUsers, setFilteredUsers] = useState<User[]>();
useEffect(() => {
if (users.length > 0) {
setFilteredUsers(users);
}
}, [users]);
const [deleteUserModal, setDeleteUserModal] = useState<UserModal>({
isOpen: false,
userId: null,
});
const [inviteModal, setInviteModal] = useState(false);
return (
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">
@@ -40,6 +80,195 @@ export default function Billing() {
</a>
</p>
</div>
<div className="flex items-center gap-2 w-full rounded-md h-8 mt-5">
<p className="truncate w-full pr-7 text-3xl font-thin">
{t("manage_seats")}
</p>
</div>
<div className="divider my-3"></div>
<div className="flex items-center justify-between gap-2 mb-3 relative">
<div>
<label
htmlFor="search-box"
className="inline-flex items-center w-fit absolute left-1 pointer-events-none rounded-md p-1 text-primary"
>
<i className="bi-search"></i>
</label>
<input
id="search-box"
type="text"
placeholder={t("search_users")}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
if (users) {
setFilteredUsers(
users.filter((user: any) =>
JSON.stringify(user)
.toLowerCase()
.includes(e.target.value.toLowerCase())
)
);
}
}}
className="border border-neutral-content bg-base-200 focus:border-primary py-1 rounded-md pl-9 pr-2 w-full max-w-[15rem] md:w-[15rem] md:max-w-full duration-200 outline-none"
/>
</div>
<div className="flex gap-3">
<div
onClick={() => setInviteModal(true)}
className="flex items-center btn btn-accent dark:border-violet-400 text-white btn-sm px-2 h-[2.15rem] relative"
>
<p>{t("invite_user")}</p>
<i className="bi-plus text-2xl"></i>
</div>
</div>
</div>
<div className="border rounded-md shadow border-neutral-content">
<table className="table bg-base-300 rounded-md">
<thead>
<tr className="sm:table-row hidden border-b-neutral-content">
{process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && (
<th>{t("email")}</th>
)}
{process.env.NEXT_PUBLIC_STRIPE === "true" && (
<th>{t("status")}</th>
)}
<th>{t("date_added")}</th>
</tr>
</thead>
<tbody>
{filteredUsers?.map((user, index) => (
<tr
key={index}
className={clsx(
"group border-b-neutral-content duration-100 w-full relative flex flex-col sm:table-row",
user.id !== account.id &&
"hover:bg-neutral-content hover:bg-opacity-30"
)}
>
{process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && (
<td className="truncate max-w-full" title={user.email || ""}>
<p className="sm:hidden block text-neutral text-xs font-bold mb-2">
{t("email")}
</p>
<p>{user.email}</p>
</td>
)}
{process.env.NEXT_PUBLIC_STRIPE === "true" && (
<td>
<p className="sm:hidden block text-neutral text-xs font-bold mb-2">
{t("status")}
</p>
{user.emailVerified ? (
<p className="font-bold px-2 bg-green-600 text-white rounded-md w-fit">
{t("active")}
</p>
) : (
<p className="font-bold px-2 bg-neutral-content rounded-md w-fit">
{t("pending")}
</p>
)}
</td>
)}
<td>
<p className="sm:hidden block text-neutral text-xs font-bold mb-2">
{t("date_added")}
</p>
<p className="whitespace-nowrap">
{new Date(user.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})}
</p>
</td>
{user.id !== account.id && (
<td className="relative">
<div
className={`dropdown dropdown-bottom font-normal dropdown-end absolute right-[0.35rem] top-[0.35rem]`}
>
<div
tabIndex={0}
role="button"
onMouseDown={dropdownTriggerer}
className="btn btn-ghost btn-sm btn-square duration-100"
>
<i
className={"bi bi-three-dots text-lg text-neutral"}
></i>
</div>
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box mt-1">
{!user.emailVerified ? (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(
document?.activeElement as HTMLElement
)?.blur();
signIn("invite", {
email: user.email,
callbackUrl: "/member-onboarding",
redirect: false,
}).then(() =>
toast.success(t("resend_invite_success"))
);
}}
className="whitespace-nowrap"
>
{t("resend_invite")}
</div>
</li>
) : undefined}
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setDeleteUserModal({
isOpen: true,
userId: user.id,
});
}}
className="whitespace-nowrap"
>
{t("remove_user")}
</div>
</li>
</ul>
</div>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
<p className="text-sm text-center font-bold mt-3">
{t(
account?.subscription?.quantity === 1
? "seat_purchased"
: "seats_purchased",
{ count: account?.subscription?.quantity }
)}
</p>
{inviteModal && <InviteModal onClose={() => setInviteModal(false)} />}
{deleteUserModal.isOpen && deleteUserModal.userId && (
<DeleteUserModal
onClose={() => setDeleteUserModal({ isOpen: false, userId: null })}
userId={deleteUserModal.userId}
/>
)}
</SettingsLayout>
);
}

View File

@@ -9,8 +9,6 @@ import getServerSideProps from "@/lib/client/getServerSideProps";
import { Trans, useTranslation } from "next-i18next";
import { useUser } from "@/hooks/store/user";
const stripeEnabled = process.env.NEXT_PUBLIC_STRIPE === "true";
export default function Subscribe() {
const { t } = useTranslation();
const [submitLoader, setSubmitLoader] = useState(false);
@@ -23,13 +21,13 @@ export default function Subscribe() {
const { data: user = {} } = useUser();
useEffect(() => {
const hasInactiveSubscription =
user.id && !user.subscription?.active && stripeEnabled;
if (session.status === "authenticated" && !hasInactiveSubscription) {
if (
session.status === "authenticated" &&
user.id &&
(user?.subscription?.active || user.parentSubscription?.active)
)
router.push("/dashboard");
}
}, [session.status]);
}, [session.status, user]);
async function submit() {
setSubmitLoader(true);
@@ -40,6 +38,8 @@ export default function Subscribe() {
const data = await res.json();
router.push(data.response);
toast.dismiss(redirectionToast);
}
return (

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "referredBy" TEXT;

View File

@@ -51,6 +51,7 @@ model User {
archiveAsPDF Boolean @default(true)
archiveAsWaybackMachine Boolean @default(false)
isPrivate Boolean @default(false)
referredBy String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}

View File

@@ -0,0 +1,420 @@
{
"user_administration": "Benutzer Administration",
"search_users": "Suche nach Benutzern",
"no_users_found": "Keine Benutzer gefunden.",
"no_user_found_in_search": "Keine Benutzer mit den angegebenen Suchparametern gefunden.",
"username": "Benutzername",
"email": "E-Mail",
"subscribed": "Abonniert",
"created_at": "Erstellt am",
"not_available": "N/A",
"check_your_email": "Bitte E-Mail prüfen.",
"authenticating": "Verifizieren...",
"verification_email_sent": "Verifizierungs E-Mail gesendet.",
"verification_email_sent_desc": "Ein Anmeldelink wurde per E-Mail versendet. Bitte auch den Spam-Ordner prüfen.",
"resend_email": "E-Mail erneut senden",
"invalid_credentials": "Ungültige Anmeldedaten.",
"fill_all_fields": "Bitte alle Felder ausfüllen.",
"enter_credentials": "Anmeldedaten eingeben",
"username_or_email": "Benutzername oder E-Mail",
"password": "Passwort",
"confirm_password": "Passwort bestätigen",
"forgot_password": "Passwort vergessen?",
"login": "Login",
"or_continue_with": "Oder fortfahren mit",
"new_here": "Neu hier?",
"sign_up": "Registrieren",
"sign_in_to_your_account": "Bei Ihrem Konto anmelden",
"dashboard_desc": "Ein kurzer Überblick über Ihre Daten",
"link": "Link",
"links": "Links",
"collection": "Sammlung",
"collections": "Sammlungen",
"tag": "Tag",
"tags": "Tags",
"recent": "Neueste",
"recent_links_desc": "Kürzlich hinzugefügte Links",
"view_all": "Alle anzeigen",
"view_added_links_here": "Sehen Sie hier Ihre kürzlich hinzugefügten Links!",
"view_added_links_here_desc": "In diesem Bereich werden Ihre zuletzt hinzugefügten Links in allen Sammlungen, auf die Sie Zugriff haben, angezeigt.",
"add_link": "Neuen Link hinzufügen",
"import_links": "Links importieren",
"from_linkwarden": "Von Linkwarden",
"from_html": "Von Lesezeichen HTML-Datei",
"from_wallabag": "Von Wallabag (JSON-Datei)",
"pinned": "Angepinnt",
"pinned_links_desc": "Deine angepinnten Links",
"pin_favorite_links_here": "Pin deine Favoriten-Links hier!",
"pin_favorite_links_here_desc": "Du kannst deine Lieblingslinks anheften, indem du auf die drei Punkte auf jedem Link klickst und dann auf An Dashboard anheften.",
"sending_password_link": "Link zur Wiederherstellung des Passworts senden...",
"password_email_prompt": "Gib deine E-Mail-Adresse ein, damit wir dir einen Link schicken können, um ein neues Passwort zu erstellen.",
"send_reset_link": "Link zum Zurücksetzen senden",
"reset_email_sent_desc": "Prüfe deine E-Mail auf einen Link zum Zurücksetzen deines Passworts. Wenn er nicht innerhalb weniger Minuten erscheint, prüfe deinen Spam-Ordner.",
"back_to_login": "Zurück zum Login",
"email_sent": "E-Mail gesendet!",
"passwords_mismatch": "Passwörter stimmen nicht überein.",
"password_too_short": "Passwort muss mindestens 8 Zeichen haben.",
"creating_account": "Erstelle Account...",
"account_created": "Account erstellt!",
"trial_offer_desc": "Schalte {{count}} Tage Premium Service kostenlos frei!",
"register_desc": "Neuen Account erstellen",
"registration_disabled_desc": "Die Registrierung ist für diese Instanz deaktiviert. Bei Problemen wende dich bitte an den Administrator.",
"enter_details": "Details eingeben",
"display_name": "Anzeigename",
"sign_up_agreement": "Mit der Anmeldung stimmst du unseren <0>Nutzungsbedingungen</0> und <1>Datenschutzbedingungen</1> zu.",
"need_help": "Hilfe benötigt?",
"get_in_touch": "Kontakt aufnehmen",
"already_registered": "Du hast bereits einen Account?",
"deleting_selections": "Lösche Auswahl...",
"links_deleted": "{{count}} Links gelöscht.",
"link_deleted": "1 Link gelöscht.",
"links_selected": "{{count}} Links ausgewählt",
"link_selected": "1 Link ausgewählt",
"nothing_selected": "Nichts ausgewählt",
"edit": "Bearbeiten",
"delete": "Löschen",
"nothing_found": "Nichts gefunden.",
"redirecting_to_stripe": "Weiterleitung zu Stripe...",
"subscribe_title": "Linkwarden abonnieren!",
"subscribe_desc": "Sie werden zu Stripe weitergeleitet, bei Problemen kontaktiere uns bitte unter <0>support@linkwarden.app</0>.",
"monthly": "Monatlich",
"yearly": "Jährlich",
"discount_percent": "{{percent}}% Rabatt",
"billed_monthly": "Monatliche Abrechnung",
"billed_yearly": "Jährliche Abrechnung",
"total": "Gesamt",
"total_annual_desc": "{{count}}-Tage kostenlos testen, dann ${{annualPrice}} Jährlich",
"total_monthly_desc": "{{count}}-Tage kostenlos testen, dann ${{monthlyPrice}} monatlich",
"plus_tax": "+ Mehrwertsteuer, falls zutreffend",
"complete_subscription": "Abonnement abschließen",
"sign_out": "Abmelden",
"access_tokens": "Zugangstokens",
"access_tokens_description": "Zugangstoken können verwendet werden, um von anderen Anwendungen und Diensten aus auf Linkwarden zuzugreifen, ohne Benutzername und Passwort anzugeben.",
"new_token": "Neuer Zugangstoken",
"name": "Name",
"created_success": "Erstellt!",
"created": "Erstellt",
"expires": "Läuft ab",
"accountSettings": "Account Einstellungen",
"language": "Sprache",
"profile_photo": "Profilbild",
"upload_new_photo": "Neues Foto hochladen...",
"remove_photo": "Foto löschen",
"make_profile_private": "Profil privat machen",
"profile_privacy_info": "Hier wird eingeschränkt, wer dich finden und zu neuen Sammlungen hinzufügen kann.",
"whitelisted_users": "Benutzer auf der Whitelist",
"whitelisted_users_info": "Bitte gib den Benutzernamen der Benutzer an, denen du Sichtbarkeit für dein Profil gewähren möchtest. Getrennt durch Komma.",
"whitelisted_users_placeholder": "Dein Profil ist im Moment für alle versteckt...",
"save_changes": "Änderungen speichern",
"import_export": "Import & Export",
"import_data": "Importiere Deine Daten von anderen Plattformen.",
"download_data": "Lade deine Daten sofort herunter.",
"export_data": "Exportiere Daten",
"delete_account": "Account löschen",
"delete_account_warning": "Dadurch werden ALLE Links, Sammlungen, Tags und archivierten Daten, die Du besitzt, endgültig gelöscht.",
"cancel_subscription_notice": "Es wird auch dein Abonnement kündigen.",
"account_deletion_page": "Seite zur Account-Löschung",
"applying_settings": "Einstellungen übernehmen...",
"settings_applied": "Einstellungen übernommen!",
"email_change_request": "E-Mail-Änderungsanfrage gesendet. Bitte bestätige die neue E-Mail-Adresse.",
"image_upload_no_file_error": "Keine Datei ausgewählt. Bitte wählen Sie ein Bild zum Hochladen.",
"image_upload_size_error": "Bitte wähle eine PNG- oder JPEG-Datei, die kleiner als 1 MB ist.",
"image_upload_format_error": "Ungültiges Dateiformat",
"importing_bookmarks": "Importiere Lesezeichen...",
"import_success": "Lesezeichen importiert! Seite neu laden...",
"more_coming_soon": "Demnächst mehr!",
"billing_settings": "Rechnungs-Einstellungen",
"manage_subscription_intro": "Um dein Abonnement zu verwalten/kündigen, besuche das",
"billing_portal": "Abrechnungsportal",
"help_contact_intro": "Wenn du noch Hilfe brauchst oder ein Problem hast, kannst du dich gerne an uns wenden:",
"fill_required_fields": "Bitte fülle die erforderlichen Felder aus.",
"deleting_message": "Alles löschen, bitte warten...",
"delete_warning": "Dadurch werden alle Links, Sammlungen, Tags und archivierten Daten, die du besitzt, dauerhaft gelöscht. Außerdem wirst du abgemeldet. Diese Aktion ist unwiderruflich!",
"optional": "Optional",
"feedback_help": "(aber es hilft uns wirklich, besser zu werden!)",
"reason_for_cancellation": "Grund für die Kündigung",
"please_specify": "Bitte angeben",
"customer_service": "Kundenservice",
"low_quality": "Geringe Qualität",
"missing_features": "Fehlende Funktionen",
"switched_service": "Anbieterwechsel",
"too_complex": "Zu kompliziert",
"too_expensive": "Zu teuer",
"unused": "Nicht benutzt",
"other": "Sonstiges",
"more_information": "Weitere Informationen (je mehr Details, desto hilfreicher wäre es)",
"feedback_placeholder": "Ich benötigte z. B. eine Funktion, die...",
"delete_your_account": "Dein Konto löschen",
"change_password": "Passwort wechseln",
"password_length_error": "Passwörter müssen mindestens 8 Zeichen lang sein.",
"applying_changes": "Übernehmen...",
"password_change_instructions": "Um das Passwort zu ändern, fülle bitte das folgende Formular aus. Das Passwort sollte mindestens 8 Zeichen lang sein.",
"old_password": "Altes Passwort",
"new_password": "Neues Passwort",
"preference": "Darstellung",
"select_theme": "Darstellung auswählen",
"dark": "Dunkel",
"light": "Hell",
"archive_settings": "Archiv Einstellungen",
"formats_to_archive": "Format zum Archivieren/Aufbewahren von Webseiten:",
"screenshot": "Screenshot",
"pdf": "PDF",
"archive_org_snapshot": "Archive.org Snapshot",
"link_settings": "Link Einstellungen",
"prevent_duplicate_links": "Doppelte Links verhindern",
"clicking_on_links_should": "Das Anklicken von Links soll:",
"open_original_content": "Den Original-Inhalt öffnen",
"open_pdf_if_available": "Das PDF öffnen, wenn verfügbar",
"open_readable_if_available": "Lesenansicht öffnen, falls verfügbar",
"open_screenshot_if_available": "Screenshot anzeigen, falls verfügbar",
"open_webpage_if_available": "Webseiten-Kopie öffnen, falls verfügbar",
"tag_renamed": "Tag umbenannt!",
"tag_deleted": "Tag gelöscht!",
"rename_tag": "Tag umbenennen",
"delete_tag": "Tag löschen",
"list_created_with_linkwarden": "Liste erstellt mit Linkwarden",
"by_author": "Von {{author}}.",
"by_author_and_other": "Von {{author}} und {{count}} anderem.",
"by_author_and_others": "Von {{author}} und {{count}} anderen.",
"search_count_link": "Durchsuche {{count}} Link",
"search_count_links": "Durchsuche {{count}} Links",
"collection_is_empty": "Diese Sammlung ist leer...",
"all_links": "Alle Links",
"all_links_desc": "Links aus allen Sammlungen",
"you_have_not_added_any_links": "Du hast noch keine Links erstellt",
"collections_you_own": "Deine Sammlungen",
"new_collection": "Neue Sammlung",
"other_collections": "Andere Sammlung",
"other_collections_desc": "Gemeinsame Sammlungen, bei denen du Mitglied bist",
"showing_count_results": "Zeige {{count}} Ergebnisse",
"showing_count_result": "Zeige {{count}} Ergebnis",
"edit_collection_info": "Sammlungsinformationen bearbeiten",
"share_and_collaborate": "Teilen und Zusammenarbeiten",
"view_team": "Team ansehen",
"team": "Team",
"create_subcollection": "Untersammlung erstellen",
"delete_collection": "Lösche Sammlung",
"leave_collection": "Verlasse Sammlung",
"email_verified_signing_out": "E-Mail verifiziert. Abmelden...",
"invalid_token": "Ungültiger Token.",
"sending_password_recovery_link": "Passwort-Wiederherstellungslink senden...",
"please_fill_all_fields": "Bitte alle Felder ausfüllen.",
"password_updated": "Passwort aktualisiert!",
"reset_password": "Passwort zurücksetzen",
"enter_email_for_new_password": "Bitte fülle deine E-Mail-Adresse aus, damit wir dir einen Link schicken können, um ein neues Passwort zu erstellen.",
"update_password": "Aktualisiere Passwort",
"password_successfully_updated": "Dein Passwort wurde erfolgreich aktualisiert.",
"user_already_member": "Benutzer existiert bereits.",
"you_are_already_collection_owner": "Du bist bereits der Eigentümer der Sammlung.",
"date_newest_first": "Datum (Neueste zuerst)",
"date_oldest_first": "Datum (Älteste zuerst)",
"name_az": "Name (A-Z)",
"name_za": "Name (Z-A)",
"description_az": "Beschreibung (A-Z)",
"description_za": "Beschreibung (Z-A)",
"all_rights_reserved": "© {{date}} <0>Linkwarden</0>. Alle Rechte vorbehalten.",
"you_have_no_collections": "Du hast keine Sammlungen...",
"you_have_no_tags": "Du hast keine Tags...",
"cant_change_collection_you_dont_own": "Du kannst keine Änderungen an einer Sammlung vornehmen, die du nicht besitzt.",
"account": "Account",
"billing": "Abrechnung",
"linkwarden_version": "Linkwarden {{version}}",
"help": "Hilfe",
"github": "GitHub",
"twitter": "Twitter",
"mastodon": "Mastodon",
"link_preservation_in_queue": "Die Link- Konservierung ist derzeit in der Warteschlange",
"check_back_later": "Bitte überprüfen das Ergebnis zu einem späteren Zeitpunkt",
"there_are_more_formats": "Es gibt noch mehr konservierte Formate in der Warteschlange",
"settings": "Einstellungen",
"switch_to": "Wechsle zu {{theme}}",
"logout": "Abmelden",
"start_journey": "Starte deine Reise, indem du einen neuen Link anlegst!",
"create_new_link": "Neuen Link anlegen",
"new_link": "Neuer Link",
"create_new": "Neu anlegen...",
"pwa_install_prompt": "Installiere Linkwarden auf deinem Startbildschirm für einen schnelleren Zugriff und ein besseres Erlebnis. <0>Mehr erfahren</0>",
"full_content": "Vollständiger Inhalt",
"slower": "Langsamer",
"new_version_announcement": "Schau, was neu ist in <0>Linkwarden {{Version}}!</0>",
"creating": "Erstellen...",
"upload_file": "Datei hochladen",
"file": "Datei",
"file_types": "PDF, PNG, JPG (Maximalgröße {{size}} MB)",
"description": "Beschreibung",
"auto_generated": "Wird automatisch generiert, wenn nichts angegeben wird.",
"example_link": "z.B. Beispiel-Link",
"hide": "Verstecken",
"more": "Mehr",
"options": "Optionen",
"description_placeholder": "Notizen, Gedanken, usw.",
"deleting": "Lösche...",
"token_revoked": "Token widerrufen.",
"revoke_token": "Token widerrufen",
"revoke_confirmation": "Bist du sicher, dass du dieses Zugangs-Token widerrufen möchtest? Alle Anwendungen oder Dienste, die dieses Token verwenden, können dann nicht mehr auf Linkwarden zugreifen.",
"revoke": "Widerrufen",
"sending_request": "Anfrage senden...",
"link_being_archived": "Link wird archiviert...",
"preserved_formats": "Konservierte Formate",
"available_formats": "Die folgenden Formate sind für diesen Link verfügbar",
"readable": "Leseansicht",
"preservation_in_queue": "Linkkonservierung ist in der Warteschlange",
"view_latest_snapshot": "Aktuellen Schnappschuss auf archive.org ansehen",
"refresh_preserved_formats": "Konservierte Formate aktualisieren",
"this_deletes_current_preservations": "Dies löscht die aktuellen Konservierungen",
"create_new_user": "Neuen Benutzer erstellen",
"placeholder_johnny": "Johnny",
"placeholder_email": "johnny@example.com",
"placeholder_john": "john",
"user_created": "Benutzer erstellt!",
"fill_all_fields_error": "Bitte alle Felder ausfüllen.",
"password_change_note": "<0>Hinweis:</0> Bitte informiere den Benutzer, dass er sein Passwort ändern muss.",
"create_user": "Benutzer erstellen",
"creating_token": "Token anlegen...",
"token_created": "Token erstellt!",
"access_token_created": "Zugangstoken erstellt",
"token_creation_notice": "Der neue Token wurde erstellt. Bitte kopiere ihn und bewahre ihn an einem sicheren Ort auf. Er wird nicht mehr zu sehen sein.",
"copied_to_clipboard": "In die Zwischenablage kopiert!",
"copy_to_clipboard": "Kopieren in Zwischenablage",
"create_access_token": "Zugangs-Token erstellen",
"expires_in": "Läuft ab in",
"token_name_placeholder": "z. B. für die iOS-Verknüpfung",
"create_token": "Zugangs-Token erstellen",
"7_days": "7 Tage",
"30_days": "30 Tage",
"60_days": "60 Tage",
"90_days": "90 Tage",
"no_expiration": "Kein Ablaufdatum",
"creating_link": "Link anlegen...",
"link_created": "Link angelegt!",
"link_name_placeholder": "Wird automatisch generiert, wenn nicht angegeben.",
"link_url_placeholder": "z.B. http://example.com/",
"link_description_placeholder": "Notizen, Gedanken, usw.",
"more_options": "Mehr Optionen",
"hide_options": "Optionen ausblenden",
"create_link": "Link erstellen",
"new_sub_collection": "Neue Untersammlung",
"for_collection": "Für {{name}}",
"create_new_collection": "Neue Sammlung erstellen",
"color": "Farbe",
"reset_defaults": "Zurücksetzen auf Standard",
"updating_collection": "Aktualisiere Sammlung...",
"collection_name_placeholder": "z.B. Beispiel Sammlung",
"collection_description_placeholder": "Zweck dieser Sammlung...",
"create_collection_button": "Sammlung erstellen",
"password_change_warning": "Bitte bestätige dein Passwort, bevor du deine E-Mail Adresse änderst.",
"stripe_update_note": " Wenn Du dieses Feld aktualisierst, wird auch Deine Rechnungs-E-Mail auf Stripe geändert.",
"sso_will_be_removed_warning": "Wenn Du deine E-Mail-Adresse änderst, werden alle bestehenden {{service}} SSO-Verbindungen entfernt werden.",
"old_email": "Alte E-Mail",
"new_email": "Neue E-Mail",
"confirm": "Bestätigen",
"edit_link": "Link bearbeiten",
"updating": "Aktualisiere...",
"updated": "Aktualisiert!",
"placeholder_example_link": "z.B. Beispiel-Link",
"make_collection_public": "Sammlung veröffentlichen",
"make_collection_public_checkbox": "Diese Sammlung öffentlich machen",
"make_collection_public_desc": "So kann jeder diese Sammlung und ihre Nutzer einsehen.",
"sharable_link": "Teilbarer Link",
"copied": "Kopiert!",
"members": "Mitglieder",
"add_member_placeholder": "Benutzer per E-Mail oder Benutzernamen hinzufügen",
"owner": "Besitzer",
"admin": "Admin",
"contributor": "Mitwirkende",
"viewer": "Leser",
"viewer_desc": "Schreibgeschützter Zugriff",
"contributor_desc": "Kann Links anzeigen und erstellen",
"admin_desc": "Voller Zugriff auf alle Links",
"remove_member": "Mitglied entfernen",
"placeholder_example_collection": "z. B. Beispiel Sammlung",
"placeholder_collection_purpose": "Zweck dieser Sammlung...",
"deleting_user": "Lösche...",
"user_deleted": "Benutzer entfernt.",
"delete_user": "Benutzer entfernen",
"confirm_user_deletion": "Bist du sicher, dass du diesen Benutzer entfernen möchtest?",
"irreversible_action_warning": "Diese Maßnahme ist nicht umkehrbar!",
"delete_confirmation": "Löschen, ich weiß, was ich tue",
"delete_link": "Link entfernen",
"deleted": "Entfernt.",
"link_deletion_confirmation_message": "Bist du sicher, dass du diesen Link entfernen möchtest?",
"warning": "Warnung",
"irreversible_warning": "Diese Aktion ist nicht umkehrbar!",
"shift_key_tip": "Halte die Umschalttaste gedrückt, während du auf „Löschen“ klickst, um diese Bestätigung in Zukunft zu umgehen.",
"deleting_collection": "Lösche...",
"collection_deleted": "Sammlung gelöscht.",
"confirm_deletion_prompt": "Zur Bestätigung tippe „{{Name}}“ in das Feld unten:",
"type_name_placeholder": "Tippe „{{Name}}“ hier.",
"deletion_warning": "Wenn Du diese Sammlung löschst, wird ihr gesamter Inhalt unwiderruflich gelöscht und sie wird für jeden unzugänglich, auch für Mitglieder mit vorherigem Zugriff.",
"leave_prompt": "Klicke auf die Schaltfläche unten, um die aktuelle Sammlung zu verlassen.",
"leave": "Verlassen",
"edit_links": "Bearbeite {{count}} Links",
"move_to_collection": "Zur Sammlung verschieben",
"add_tags": "Tags hinzufügen",
"remove_previous_tags": "Vorherige Tags entfernen",
"delete_links": "Lösche {{count}} Links",
"links_deletion_confirmation_message": "Willst du wirklich {{count}} Links löschen? ",
"warning_irreversible": "Warnung: Diese Aktion ist unumkehrbar!",
"shift_key_instruction": "Halte die Umschalttaste gedrückt, während du auf „Löschen“ klickst, um diese Bestätigung in Zukunft zu umgehen.",
"link_selection_error": "Du hast nicht die Berechtigung, diesen Inhalt zu bearbeiten oder zu löschen.",
"no_description": "Keine Beschreibung vorhanden.",
"applying": "Übernehme...",
"unpin": "Abheften",
"pin_to_dashboard": "An das Dashboard anheften",
"show_link_details": "Linkdetails anzeigen",
"link_pinned": "Link angeheftet!",
"link_unpinned": "Link abhgeheftet!",
"webpage": "Webseite",
"server_administration": "Server-Verwaltung",
"all_collections": "Alle Sammlungen",
"dashboard": "Dashboard",
"demo_title": "Nur Demo",
"demo_desc": "Dies ist nur eine Demo-Instanz von Linkwarden und Uploads sind deaktiviert.",
"demo_desc_2": "Wenn Du die Vollversion ausprobieren möchtest, kannst Du Dich für eine kostenlose Testversion anmelden unter:",
"demo_button": "Login als Demo-Benutzer",
"regular": "Regular",
"thin": "Dünn",
"bold": "Fett",
"fill": "Ausfüllen",
"duotone": "Duotone",
"light_icon": "Hell",
"search": "Suche",
"set_custom_icon": "Benutzerdefiniertes Symbol festlegen",
"view": "Ansicht",
"show": "anzeigen",
"image": "Bild",
"icon": "Symbol",
"date": "Datum",
"preview_unavailable": "Vorschau nicht verfügbar",
"saved": "Gesichert",
"untitled": "Unbenannt",
"no_tags": "Keine tags.",
"no_description_provided": "Keine Beschreibung vorhanden.",
"change_icon": "Symbol ändern",
"upload_preview_image": "Vorschaubild hochladen",
"columns": "Spalten",
"default": "Standard",
"invalid_url_guide": "Bitte geben Sie eine gültige Adresse für den Link ein. (Sie sollte mit http/https beginnen)",
"email_invalid": "Bitte geben Sie eine gültige E-Mail Adresse ein.",
"username_invalid_guide": "Der Benutzername muss mindestens 3 Zeichen lang sein, Leerzeichen und Sonderzeichen sind nicht erlaubt.",
"team_management": "Team Management",
"invite_user": "Benutzer einladen",
"invite_users": "Benutzer einladen",
"invite_user_desc": "Um jemanden in Ihr Team einzuladen, geben Sie bitte unten die E-Mail-Adresse ein:",
"invite_user_note": "Bitte beachten Sie, dass mit der Annahme der Einladung ein zusätzlicher Platz erworben wird und dieser automatisch Ihrem Konto in Rechnung gestellt wird.",
"send_invitation": "Einladung senden",
"learn_more": "Mehr erfahren",
"invitation_desc": "{{owner}} hat Sie eingeladen, Linkwarden beizutreten. \nUm fortzufahren, schließen Sie bitte die Einrichtung Ihres Kontos ab.",
"invitation_accepted": "Einladung akzeptiert!",
"status": "Status",
"pending": "Offen",
"active": "Aktiv",
"manage_seats": "Plätze verwalten",
"seats_purchased": "{{count}} gekaufte Plätze",
"date_added": "Datum hinzugefügt",
"resend_invite": "Einladung erneut senden",
"resend_invite_success": "Einladung verschickt!",
"remove_user": "Benutzer entfernen",
"continue_to_dashboard": "Weiter zum Dashboard",
"confirm_user_removal_desc": "Sie müssen ein Abonnement haben, um wieder Zugriff auf Linkwarden zu haben."
}

View File

@@ -117,6 +117,7 @@
"applying_settings": "Applying settings...",
"settings_applied": "Settings Applied!",
"email_change_request": "Email change request sent. Please verify the new email address.",
"image_upload_no_file_error": "No file selected. Please choose an image to upload.",
"image_upload_size_error": "Please select a PNG or JPEG file that's less than 1MB.",
"image_upload_format_error": "Invalid file format.",
"importing_bookmarks": "Importing bookmarks...",
@@ -255,7 +256,7 @@
"sending_request": "Sending request...",
"link_being_archived": "Link is being archived...",
"preserved_formats": "Preserved Formats",
"available_formats": "The following formats are available for this link:",
"available_formats": "The following formats are available for this link",
"readable": "Readable",
"preservation_in_queue": "Link preservation is in the queue",
"view_latest_snapshot": "View latest snapshot on archive.org",
@@ -296,7 +297,7 @@
"for_collection": "For {{name}}",
"create_new_collection": "Create a New Collection",
"color": "Color",
"reset": "Reset",
"reset_defaults": "Reset to Defaults",
"updating_collection": "Updating Collection...",
"collection_name_placeholder": "e.g. Example Collection",
"collection_description_placeholder": "The purpose of this Collection...",
@@ -314,10 +315,10 @@
"make_collection_public": "Make Collection Public",
"make_collection_public_checkbox": "Make this a public collection",
"make_collection_public_desc": "This will allow anyone to view this collection and it's users.",
"sharable_link_guide": "Sharable Link (Click to copy)",
"sharable_link": "Sharable Link",
"copied": "Copied!",
"members": "Members",
"members_username_placeholder": "Username (without the '@')",
"add_member_placeholder": "Add members by email or username",
"owner": "Owner",
"admin": "Admin",
"contributor": "Contributor",
@@ -361,7 +362,6 @@
"unpin": "Unpin",
"pin_to_dashboard": "Pin to Dashboard",
"show_link_details": "Show Link Details",
"hide_link_details": "Hide Link Details",
"link_pinned": "Link Pinned!",
"link_unpinned": "Link Unpinned!",
"webpage": "Webpage",
@@ -371,5 +371,63 @@
"demo_title": "Demo Only",
"demo_desc": "This is only a demo instance of Linkwarden and uploads are disabled.",
"demo_desc_2": "If you want to try out the full version, you can sign up for a free trial at:",
"demo_button": "Login as demo user"
"demo_button": "Login as demo user",
"regular": "Regular",
"thin": "Thin",
"bold": "Bold",
"fill": "Fill",
"duotone": "Duotone",
"light_icon": "Light",
"search": "Search",
"set_custom_icon": "Set Custom Icon",
"view": "View",
"show": "Show",
"image": "Image",
"icon": "Icon",
"date": "Date",
"preview_unavailable": "Preview Unavailable",
"saved": "Saved",
"untitled": "Untitled",
"no_tags": "No tags.",
"no_description_provided": "No description provided.",
"change_icon": "Change Icon",
"upload_preview_image": "Upload Preview Image",
"columns": "Columns",
"default": "Default",
"invalid_url_guide":"Please enter a valid Address for the Link. (It should start with http/https)",
"email_invalid": "Please enter a valid email address.",
"username_invalid_guide": "Username has to be at least 3 characters, no spaces and special characters are allowed.",
"team_management": "Team Management",
"invite_user": "Invite User",
"invite_users": "Invite Users",
"invite_user_desc": "To invite someone to your team, please enter their email address below:",
"invite_user_note": "Please note that once the invitation is accepted, an additional seat will be purchased and your account will automatically be billed for this addition.",
"invite_user_price": "The cost of each seat is ${{price}} per month or ${{priceAnnual}} per year, depending on your current subscription plan.",
"send_invitation": "Send Invitation",
"learn_more": "Learn more",
"invitation_desc": "{{owner}} invited you to join Linkwarden. \nTo continue, please finish setting up your account.",
"invitation_accepted": "Invitation Accepted!",
"status": "Status",
"pending": "Pending",
"active": "Active",
"manage_seats": "Manage Seats",
"seats_purchased": "{{count}} seats purchased",
"seat_purchased": "{{count}} seat purchased",
"date_added": "Date Added",
"resend_invite": "Resend Invitation",
"resend_invite_success": "Invitation Resent!",
"remove_user": "Remove User",
"continue_to_dashboard": "Continue to Dashboard",
"confirm_user_removal_desc": "They will need to have a subscription to access Linkwarden again.",
"click_out_to_apply": "Click outside to apply",
"submit": "Submit",
"thanks_for_feedback": "Thanks for your feedback!",
"quick_survey": "Quick Survey",
"how_did_you_discover_linkwarden": "How did you discover Linkwarden?",
"rather_not_say": "Rather not say",
"search_engine": "Search Engine (Google, Bing, etc.)",
"reddit": "Reddit",
"lemmy": "Lemmy",
"people_recommendation": "Recommendation (Friend, Family, etc.)",
"open_all_links": "Open all Links"
}

View File

@@ -0,0 +1,375 @@
{
"user_administration": "Administración de usuarios",
"search_users": "Buscar usuarios",
"no_users_found": "No se ha encontrado el usuario.",
"no_user_found_in_search": "No se ha encontrado el usuario con esos parámetros de búsqueda.",
"username": "Nombre de Usuario",
"email": "Email",
"subscribed": "Suscrito",
"created_at": "Creado en",
"not_available": "N/D",
"check_your_email": "Por favor, comprueba tu correo electrónico",
"authenticating": "Autenticando...",
"verification_email_sent": "Email de verificación enviado.",
"verification_email_sent_desc": "Se ha enviado un enlace de inicio de sesión a tu correo electrónico. Si no ves el correo, revisa tu carpeta de spam.",
"resend_email": "Reenviar correo electrónico",
"invalid_credentials": "Credenciales inválidas.",
"fill_all_fields": "Por favor, rellena todos los campos.",
"enter_credentials": "Ingresa tus credenciales.",
"username_or_email": "Nombre de usuario o correo electrónico",
"password": "Contraseña",
"confirm_password": "Confirmar contraseña",
"forgot_password": "¿Has olvidado tu contraseña?",
"login": "Iniciar sesión",
"or_continue_with": "O continue con",
"new_here": "¿Nuevo por aquí?",
"sign_up": "Registrarse",
"sign_in_to_your_account": "Inicia sesión en tu cuenta.",
"dashboard_desc": "Un breve resumen de tus datos",
"link": "Enlace",
"links": "Enlaces",
"collection": "Colección",
"collections": "Colecciones",
"tag": "Etiqueta",
"tags": "Etiquetas",
"recent": "Reciente",
"recent_links_desc": "Enlaces añadidos recientemente",
"view_all": "Ver todo",
"view_added_links_here": "¡Mira todos tus enlaces añadidos recientemente aquí!",
"view_added_links_here_desc": "Esta sección mostrará tus enlaces añadidos más recientes en todas las colecciones a las que tengas acceso.",
"add_link": "Añade nuevos enlaces",
"import_links": "Importar enlaces",
"from_linkwarden": "Desde Linkwarden",
"from_html": "Desde un archivo HTML de marcadores",
"from_wallabag": "Desde Wallabag (archivo JSON)",
"pinned": "Anclado",
"pinned_links_desc": "Tus enlaces anclados",
"pin_favorite_links_here": "¡Ancla tus enlaces favoritos aquí!",
"pin_favorite_links_here_desc": "Puedes anclar tus enlaces favoritos haciendo clic en los tres puntos en cada enlace y seleccionando Anclar en el tablero.",
"sending_password_link": "Enviando enlace de recuperación de contraseña...",
"password_email_prompt": "Ingresa tu correo electrónico para que podamos enviarte un enlace para crear una nueva contraseña.",
"send_reset_link": "Enviar enlace de restablecimiento",
"reset_email_sent_desc": "Revisa tu correo electrónico para encontrar un enlace para restablecer tu contraseña. Si no aparece en unos minutos, revisa tu carpeta de spam.",
"back_to_login": "Volver al inicio de sesión",
"email_sent": "¡Email enviado!",
"passwords_mismatch": "Las contraseñas no coinciden.",
"password_too_short": "La contraseña deben tener al menos 8 caracteres.",
"creating_account": "Creando cuenta...",
"account_created": "¡Cuenta creada!",
"trial_offer_desc": "¡Desbloquea {{count}} días de servicio Premium gratis!",
"register_desc": "Crear una nueva cuenta",
"registration_disabled_desc": "El registro está deshabilitado para esta instancia. Por favor, contacta al administrador en caso de cualquier problema.",
"enter_details": "Ingresa tus datos",
"display_name": "Mostrar nombre",
"sign_up_agreement": "Al registrarte, aceptas nuestros <0>Términos de Servicio</0> y nuestra <1>Política de Privacidad</1>.",
"need_help": "¿Necesitas ayuda?",
"get_in_touch": "Contáctanos",
"already_registered": "¿Ya tienes una cuenta?",
"deleting_selections": "Eliminando seleccionados...",
"links_deleted": "{{count}} enlaces eliminados.",
"link_deleted": "1 enlace eliminado.",
"links_selected": "{{count}} enlaces seleccionados",
"link_selected": "1 enlace seleccionado",
"nothing_selected": "Nada seleccionado",
"edit": "Editar",
"delete": "Eliminar",
"nothing_found": "No se encontró nada.",
"redirecting_to_stripe": "Redirigiendo a Stripe...",
"subscribe_title": "¡Suscríbete a Linkwarden!",
"subscribe_desc": "Serás redirigido a Stripe. No dudes en ponerte en contacto con nosotros en <0>support@linkwarden.app</0> en caso de cualquier problema.",
"monthly": "Mensual",
"yearly": "Anual",
"discount_percent": "{{percent}}% de descuento",
"billed_monthly": "Facturado mensualmente",
"billed_yearly": "Facturado anualmente",
"total": "Total",
"total_annual_desc": "Prueba gratuita de {{count}} días, luego ${{annualPrice}} al año",
"total_monthly_desc": "Prueba gratuita de {{count}} días, luego ${{annualPrice}} al mes",
"plus_tax": "+ IVA si corresponde",
"complete_subscription": "Completar suscripción",
"sign_out": "Cerrar sesión",
"access_tokens": "Tokens de acceso",
"access_tokens_description": "Los tokens de acceso se pueden usar para acceder a Linkwarden desde otras aplicaciones y servicios sin revelar tu nombre de usuario y contraseña.",
"new_token": "Nuevo token de acceso",
"name": "Nombre",
"created_success": "¡Creado!",
"created": "Creado",
"expires": "Expira",
"accountSettings": "Ajustes de la cuenta",
"language": "Idioma",
"profile_photo": "Foto de perfil",
"upload_new_photo": "Sube una foto nueva...",
"remove_photo": "Eliminar foto",
"make_profile_private": "Hacer perfil privado",
"profile_privacy_info": "Esto limitará quién puede encontrarte y agregarte a nuevas colecciones.",
"whitelisted_users": "Usuarios autorizados",
"whitelisted_users_info": "Por favor, proporciona el nombre de usuario de los usuarios a los que deseas conceder visibilidad de tu perfil, separados por comas.",
"whitelisted_users_placeholder": "Tu perfil está oculto para todos en este momento...",
"save_changes": "Guardar cambios",
"import_export": "Importar y exportar",
"import_data": "Importa tus datos desde otras plataformas.",
"download_data": "Descarga tus datos al instante.",
"export_data": "Exportar datos",
"delete_account": "Eliminar cuenta",
"delete_account_warning": "Esto eliminará permanentemente TODOS los enlaces, colecciones, etiquetas y datos archivados en tu cuenta.",
"cancel_subscription_notice": "También cancelará tu suscripción.",
"account_deletion_page": "Página de eliminación de cuenta",
"applying_settings": "Aplicando configuraciones...",
"settings_applied":"¡Configuraciones aplicadas!",
"email_change_request": "Solicitud de cambio de correo electrónico enviado. Por favor, verifica el nuevo correo electrónico.",
"image_upload_size_error": "Por favor, selecciona un archivo PNG o JPEG que sea menor de 1 MB.",
"image_upload_format_error": "Formato de archivo no válido.",
"importing_bookmarks": "Importando marcadores...",
"import_success": "¡Marcadores importados! Recargando la página...",
"more_coming_soon": "¡Más próximamente!",
"billing_settings": "Ajustes de facturación",
"manage_subscription_intro": "Para gestionar/cancelar tu suscripción, visita el",
"billing_portal": "Portal de facturación",
"help_contact_intro": "Si necesitas ayuda o has encontrado algún problema, no dudes en contactarnos en:",
"fill_required_fields": "Por favor, rellena los campos obligatorios.",
"deleting_message": "Eliminando todo, por favor espera...",
"delete_warning": "Esto eliminará permanentemente todos los enlaces, colecciones, etiquetas y datos archivados en tu cuenta. También cerrará la sesión. ¡Esta acción es irreversible!",
"optional": "Opcional",
"feedback_help": "(¡pero realmente nos ayuda a mejorar!)",
"reason_for_cancellation": "Motivos de la cancelación",
"please_specify": "Por favor, especifica",
"customer_service": "Servicio al usuario",
"low_quality": "Mala calidad",
"missing_features": "Le faltan utilidades",
"switched_service": "Cambio a otra plataforma",
"too_complex": "Muy complejo",
"too_expensive": "Muy caro",
"unused": "No lo uso",
"other": "Otro",
"more_information": "Más información (cuantos más detalles, más útil será)",
"feedback_placeholder": "ej. necesitaba una función que...",
"delete_your_account": "Elliminar tu cuenta",
"change_password": "Cambiar contraseña",
"password_length_error": "La contraseña debe tener al menos 8 caracteres.",
"applying_changes": "Aplicando...",
"password_change_instructions": "Para cambiar tu contraseña, por favor completa lo siguiente. Tu contraseña debe tener al menos 8 caracteres.",
"old_password": "Contraseña actual",
"new_password": "Nueva contraseña",
"preference": "Preferencias",
"select_theme": "Seleccionar tema",
"dark": "Oscuro",
"light": "Claro",
"archive_settings": "Ajustes de archivado",
"formats_to_archive": "Formatos para archivar/conservar páginas web:",
"screenshot": "Captura de pantalla",
"pdf": "PDF",
"archive_org_snapshot": "Captura en archive.org",
"link_settings": "Ajustes de enlaces",
"prevent_duplicate_links": "Prevenir enlaces duplicados",
"clicking_on_links_should": "Hacer click en enlaces debería:",
"open_original_content": "Abrir el contenido original",
"open_pdf_if_available": "Abrir PDF, si está disponible",
"open_readable_if_available": "Abrir versión lectura, si está disponible",
"open_screenshot_if_available": "Abrir la captura de pantalla, si está disponible",
"open_webpage_if_available": "Abrir copia local de la web, si está disponible",
"tag_renamed": "¡Etiqueta renombrada!",
"tag_deleted": "¡Etiqueta eliminada!",
"rename_tag": "Renombrar etiqueta",
"delete_tag": "Eliminar etiqueta",
"list_created_with_linkwarden": "Lista generada con Linkwarden",
"by_author": "Por {{author}}.",
"by_author_and_other": "Por {{author}} y {{count}} más.",
"by_author_and_others": "Por {{author}} y {{count}} más.",
"search_count_link": "Buscar {{count}} enlace",
"search_count_links": "Buscar {{count}} enlaces",
"collection_is_empty": "Esta colección está vacía...",
"all_links": "Todos los enlaces",
"all_links_desc": "Enlaces de todas las colecciones",
"you_have_not_added_any_links": "Aún no has creado ningún enlace",
"collections_you_own": "Colecciones que te pertenecen",
"new_collection": "Nueva colección",
"other_collections": "Otras colecciones",
"other_collections_desc": "Colecciones compartidas de las que eres miembro",
"showing_count_results": "Mostrando {{count}} resultados",
"showing_count_result": "Mostrando {{count}} resultado",
"edit_collection_info": "Editar información de la colección",
"share_and_collaborate": "Compartir y colaborar",
"view_team": "Ver grupo",
"team": "Grupo",
"create_subcollection": "Crear subcolección",
"delete_collection": "Eliminar colección",
"leave_collection": "Salir de la colección",
"email_verified_signing_out": "Correo electrónico verificado. Cerrando sesión...",
"invalid_token": "Token inválido.",
"sending_password_recovery_link": "Enviando enlace de recuperación de contraseña",
"please_fill_all_fields": "Por favor, rellena todos los campos.",
"password_updated": "¡Contraseña actualizada!",
"reset_password": "Restablecer contraseña",
"enter_email_for_new_password": "Ingresa tu correo electrónico para que podamos enviarte un enlace para crear una nueva contraseña.",
"update_password": "Actualizar contraseña",
"password_successfully_updated": "Tu contraseña ha sido actualizada correctamente.",
"user_already_member": "El usuario ya existe.",
"you_are_already_collection_owner": "Ya eres el dueño de esta colección.",
"date_newest_first": "Fecha (Nuevos primero)",
"date_oldest_first": "Fecha (Antiguos primero)",
"name_az": "Nombre (A-Z)",
"name_za": "Nombre (Z-A)",
"description_az": "Descripción (A-Z)",
"description_za": "Descripción (Z-A)",
"all_rights_reserved": "© {{date}} <0>Linkwarden</0>. Todos los derechos reservados.",
"you_have_no_collections": "No tienes ninguna colección...",
"you_have_no_tags": "No tienes ninguna etiqueta...",
"cant_change_collection_you_dont_own": "No puedes hacer cambios en una colección que no te pertenece.",
"account": "Cuenta",
"billing": "Facturación",
"linkwarden_version": "Linkwarden {{version}}",
"help": "Ayuda",
"github": "GitHub",
"twitter": "Twitter",
"mastodon": "Mastodon",
"link_preservation_in_queue": "La conservación del enlace está actualmente en cola.",
"check_back_later": "Por favor, vuelve más tarde para ver el resultado.",
"there_are_more_formats": "Hay más formatos de conservación en cola.",
"settings": "Ajustes",
"switch_to": "Cambiar a {{theme}}",
"logout": "Cerrar sesión",
"start_journey": "¡Empieza tu experiencia creando un nuevo enlace!",
"create_new_link": "Crear un nuevo enlace",
"new_link": "Nuevo enlace",
"create_new": "Crear nuevo...",
"pwa_install_prompt": "Instala Linkwarden en la pantalla de inicio para un acceso más rápido y una experiencia mejorada. <0>Aprende más</0>",
"full_content": "Contenido completo",
"slower": "Más despacio",
"new_version_announcement": "¡Descubre las novedades en <0>Linkwarden {{version}}!</0>",
"creating": "Creando...",
"upload_file": "Subir archivo",
"file": "Archivo",
"file_types": "PDF, PNG, JPG (Hasta {{size}} MB)",
"description": "Descripción",
"auto_generated": "Será generado automáticamente si no se proporciona nada.",
"example_link": "ej. Enlace de ejemplo",
"hide": "Esconder",
"more": "Más",
"options": "Opciones",
"description_placeholder": "Notas, ideas, etc.",
"deleting": "Eliminando...",
"token_revoked": "Token anulado.",
"revoke_token": "Anular token",
"revoke_confirmation": "¿Estás seguro de que deseas anular este token de acceso? Cualquier aplicación o servicio que use este token ya no podrá acceder a Linkwarden con él.",
"revoke": "Anular",
"sending_request": "Enviando solicitud...",
"link_being_archived": "El enlace está siendo archivado...",
"preserved_formats": "Formatos conservados",
"available_formats": "Los siguientes formatos están disponibles para este enlace",
"readable": "Versión lectura",
"preservation_in_queue": "La conservación del enlace está en la cola.",
"view_latest_snapshot": "Ver la última captura en archive.org",
"refresh_preserved_formats": "Actualizar formatos conservados",
"this_deletes_current_preservations": "Esto elimina las conservaciones actuales.",
"create_new_user": "Crear nuevo usuario",
"placeholder_johnny": "Johnny",
"placeholder_email": "johnny@ejemplo.com",
"placeholder_john": "john",
"user_created": "¡Usuario creado!",
"fill_all_fields_error": "Por favor, rellena todos los campos.",
"password_change_note": "<0>Nota:</0> Por favor, asegúrate de informar al usuario que necesita cambiar su contraseña.",
"create_user": "Crear usuario",
"creating_token": "Creando token...",
"token_created": "¡Token creado!",
"access_token_created": "Token de acceso creado",
"token_creation_notice": "Tu nuevo token ha sido creado. Por favor, cópialo y guárdalo en un lugar seguro. No lo mostraremos de nuevo.",
"copied_to_clipboard": "¡Copiado al portapapeles!",
"copy_to_clipboard": "Copiar al portapapeles",
"create_access_token": "Crear un token de acceso",
"expires_in": "Caduca en",
"token_name_placeholder": "ej. Para el atajo de iOS",
"create_token": "Crear token de acceso",
"7_days": "7 días",
"30_days": "30 días",
"60_days": "60 días",
"90_days": "90 días",
"no_expiration": "Sin caducidad",
"creating_link": "Creando enlace...",
"link_created": "¡Enlace creado!",
"link_name_placeholder": "Se generará automáticamente si se deja en blanco.",
"link_url_placeholder": "ej. http://ejemplo.com/",
"link_description_placeholder": "Notas, ideas, etc.",
"more_options": "Más opciones",
"hide_options": "Ocultar opciones",
"create_link": "Crear enlace",
"new_sub_collection": "Nueva subcolección",
"for_collection": "Para {{name}}",
"create_new_collection": "Crear una nueva colección",
"color": "Color",
"reset": "Reestablecer",
"updating_collection": "Actualizando colección...",
"collection_name_placeholder": "ej. Colección de ejemplo",
"collection_description_placeholder": "El propósito de esta colección...",
"create_collection_button": "Crear colección",
"password_change_warning": "Por favor, confirma tu contraseña antes de cambiar tu email.",
"stripe_update_note": " Actualizar este campo cambiará también tu email de facturación en Stripe.",
"sso_will_be_removed_warning": "Si cambias tu correo electrónico, cualquier conexión SSO existente de {{service}} será eliminada.",
"old_email": "Correo electrónico actual",
"new_email": "Correo electrónico nuevo",
"confirm": "Confirmar",
"edit_link": "Editar enlace",
"updating": "Actualizando...",
"updated": "¡Actualizado!",
"placeholder_example_link": "Ej. Enlace de ejemplo",
"make_collection_public": "Hacer pública la colección",
"make_collection_public_checkbox": "Hacer pública esta colección",
"make_collection_public_desc": "Esto permitirá que cualquier persona vea esta colección y sus usuarios.",
"sharable_link_guide": "Enlace para compartir (haz clic para copiar)",
"copied": "¡Copiado!",
"members": "Participantes",
"members_username_placeholder": "Nombre de usuario (sin la '@')",
"owner": "Propietario",
"admin": "Administrador",
"contributor": "Colaborador",
"viewer": "Lector",
"viewer_desc": "Acceso de solo lectura",
"contributor_desc": "Puede ver y crear enlaces",
"admin_desc": "Acceso completo a todos los enlaces",
"remove_member": "Eliminar participante",
"placeholder_example_collection": "Ej. colección de ejemplo",
"placeholder_collection_purpose": "El propósito de esta colección...",
"deleting_user": "Eliminando...",
"user_deleted": "Usuario eliminado.",
"delete_user": "Eliminar usuario",
"confirm_user_deletion": "¿Estás seguro de que quieres eliminar este usuario?",
"irreversible_action_warning": "¡Esta acción es irreversible!",
"delete_confirmation": "Eliminar, sé lo que estoy haciendo",
"delete_link": "Eliminar enlace",
"deleted": "Eliminado.",
"link_deletion_confirmation_message": "¿Estás seguro de que quieres eliminar este enlace?",
"warning": "Advertencia",
"irreversible_warning": "¡Esta acción es irreversible!",
"shift_key_tip": "Mantén presionada la tecla 'Mayúscula' mientras haces clic en 'Eliminar' para omitir esta confirmación en el futuro.",
"deleting_collection": "Eliminando...",
"collection_deleted": "Colección eliminada.",
"confirm_deletion_prompt": "Para confirmar, escribe \"{{name}}\" en el campo a continuación:",
"type_name_placeholder": "Escribe \"{{name}}\" aquí.",
"deletion_warning": "Eliminar esta colección borrará permanentemente todo su contenido, y se volverá inaccesible para todos, incluidos los miembros con acceso previamente.",
"leave_prompt": "Haz clic en el botón de abajo para abandonar la colección actual.",
"leave": "Abandonar",
"edit_links": "Editar {{count}} enlaces",
"move_to_collection": "Mover a colección",
"add_tags": "Añadir etiquetas",
"remove_previous_tags": "Eliminar etiquetas anteriores",
"delete_links": "Eliminar {{count}} enlaces",
"links_deletion_confirmation_message": "¿Estás seguro de que deseas eliminar {{count}} enlaces? ",
"warning_irreversible": "Advertencia: ¡Esta acción es irreversible!",
"shift_key_instruction": "Mantén presionada la tecla 'Mayúscula' mientras haces clic en 'Eliminar' para omitir esta confirmación en el futuro.",
"link_selection_error": "No tienes permiso para editar o eliminar este item.",
"no_description": "No se ha proporcionado una descripción.",
"applying": "Aplicando...",
"unpin": "Desanclar",
"pin_to_dashboard": "Anclar al tablero",
"show_link_details": "Mostrar detalles del enlace",
"hide_link_details": "Ocultar detalles del enlace",
"link_pinned": "¡Enlace anclado!",
"link_unpinned": "¡Enlace desanclado!",
"webpage": "Página web",
"server_administration": "Administración del servidor",
"all_collections": "Todas las colecciones",
"dashboard": "Tablero",
"demo_title": "Solo para demostración",
"demo_desc": "Esta es solo una instancia de demostración de Linkwarden y las cargas están deshabilitadas.",
"demo_desc_2": "Si deseas probar la versión completa, puedes registrarte para una prueba gratuita en:",
"demo_button": "Iniciar sesión como usuario demo"
}

View File

@@ -255,7 +255,7 @@
"sending_request": "Envoi de la demande...",
"link_being_archived": "Le lien est en cours d'archivage...",
"preserved_formats": "Formats conservés",
"available_formats": "Les formats suivants sont disponibles pour ce lien:",
"available_formats": "Les formats suivants sont disponibles pour ce lien",
"readable": "Lisible",
"preservation_in_queue": "La préservation du lien est dans la file d'attente",
"view_latest_snapshot": "Voir le dernier instantané sur archive.org",

View File

@@ -255,7 +255,7 @@
"sending_request": "Invio richiesta...",
"link_being_archived": "Il Link è in fase di archiviazione...",
"preserved_formats": "Formati Preservati",
"available_formats": "I seguenti formati sono disponibili per questo link:",
"available_formats": "I seguenti formati sono disponibili per questo link",
"readable": "Leggibile",
"preservation_in_queue": "La preservazione del Link è in coda",
"view_latest_snapshot": "Visualizza l'ultimo snapshot su archive.org",

View File

@@ -0,0 +1,397 @@
{
"user_administration": "ユーザー管理",
"search_users": "ユーザーを検索",
"no_users_found": "ユーザーが見つかりません",
"no_user_found_in_search": "指定された検索クエリでユーザーが見つかりませんでした。",
"username": "ユーザー名",
"email": "メール",
"subscribed": "登録済",
"created_at": "作成日",
"not_available": "N/A",
"check_your_email": "メールを確認してください",
"authenticating": "認証中...",
"verification_email_sent": "確認メールが送信されました。",
"verification_email_sent_desc": "サインインリンクがあなたのメールアドレスに送信されました。メールが見つからない場合は、迷惑メールフォルダを確認してください。",
"resend_email": "メールを再送",
"invalid_credentials": "無効な資格情報です。",
"fill_all_fields": "すべての項目を入力してください",
"enter_credentials": "資格情報を入力してください",
"username_or_email": "ユーザー名またはメール",
"password": "パスワード",
"confirm_password": "パスワードを確認",
"forgot_password": "パスワードをお忘れですか?",
"login": "ログイン",
"or_continue_with": "または次で続行",
"new_here": "初めてですか?",
"sign_up": "サインアップ",
"sign_in_to_your_account": "アカウントにサインイン",
"dashboard_desc": "データの概要",
"link": "リンク",
"links": "リンク",
"collection": "コレクション",
"collections": "コレクション",
"tag": "タグ",
"tags": "タグ",
"recent": "最近",
"recent_links_desc": "最近追加されたリンク",
"view_all": "すべて表示",
"view_added_links_here": "ここで最近追加されたリンクを表示!",
"view_added_links_here_desc": "このセクションでは、アクセス可能なすべてのコレクションにわたって最近追加されたリンクが表示されます。",
"add_link": "新しいリンクを追加",
"import_links": "リンクをインポート",
"from_linkwarden": "Linkwardenから",
"from_html": "ブックマークHTMLファイルから",
"from_wallabag": "Wallabag (JSON file)から",
"pinned": "ピン留め",
"pinned_links_desc": "ピン留めされたリンク",
"pin_favorite_links_here": "お気に入りのリンクをここにピン留め!",
"pin_favorite_links_here_desc": "各リンクの三点リーダーをクリックして、[ダッシュボードにピン留め]をクリックすることで、お気に入りのリンクをピン留めできます。",
"sending_password_link": "パスワード回復リンクを送信中...",
"password_email_prompt": "新しいパスワードを作成するためのリンクを送信できるように、メールを入力してください。",
"send_reset_link": "リセットリンクを送信",
"reset_email_sent_desc": "パスワードをリセットするためのリンクがメールに送信されました。数分以内に表示されない場合は、迷惑メールフォルダを確認してください。",
"back_to_login": "ログインに戻る",
"email_sent": "メール送信済み!",
"passwords_mismatch": "パスワードが一致しません。",
"password_too_short": "パスワードは8文字以上でなければなりません。",
"creating_account": "アカウントを作成中...",
"account_created": "アカウントが作成されました!",
"trial_offer_desc": "{{count}} 日間のプレミアムサービスを無料でご利用いただけます!",
"register_desc": "新しいアカウントを作成",
"registration_disabled_desc": "このインスタンスでは登録が無効になっています。問題がある場合は、管理者にお問い合わせください。",
"enter_details": "詳細を入力してください",
"display_name": "表示名",
"sign_up_agreement": "サインアップすることにより、 <0>利用規約</0> 及び <1>プライバシーポリシー</1> に同意したものとします。",
"need_help": "ヘルプが必要ですか?",
"get_in_touch": "お問い合わせ",
"already_registered": "すでにアカウントをお持ちですか?",
"deleting_selections": "選択を削除中...",
"links_deleted": "{{count}} 個のリンクが削除されました。",
"link_deleted": "1個のリンクが削除されました。",
"links_selected": "{{count}} 個のリンクが選択されました",
"link_selected": "1個のリンクが選択されました",
"nothing_selected": "選択されていません",
"edit": "編集",
"delete": "削除",
"nothing_found": "見つかりませんでした。",
"redirecting_to_stripe": "Stripeにリダイレクトしています...",
"subscribe_title": "Linkwardenに登録",
"subscribe_desc": "Stripeにリダイレクトされます。問題がある場合は、 <0>support@linkwarden.app</0> までお問い合わせください。",
"monthly": "月額",
"yearly": "年額",
"discount_percent": "{{percent}}% Off",
"billed_monthly": "月額請求",
"billed_yearly": "年額請求",
"total": "合計",
"total_annual_desc": "{{count}} 日間の無料トライアル後、年間 {{annualPrice}}ドル が請求されます",
"total_monthly_desc": "{{count}} 日間の無料トライアル後、月額 {{monthlyPrice}}ドル が請求されます",
"plus_tax": "該当する場合は+税",
"complete_subscription": "サブスクリプションを完了",
"sign_out": "サインアウト",
"access_tokens": "アクセス トークン",
"access_tokens_description": "アクセス トークンは、ユーザー名とパスワードを公開することなく、他のアプリやサービスからLinkwardenにアクセスするために使用できます。",
"new_token": "新しいアクセス トークン",
"name": "名前",
"created_success": "作成完了!",
"created": "作成日",
"expires": "有効期限",
"accountSettings": "アカウント設定",
"language": "言語",
"profile_photo": "プロフィール写真",
"upload_new_photo": "新しい写真をアップロード...",
"remove_photo": "写真を削除",
"make_profile_private": "プロフィールを非公開にする",
"profile_privacy_info": "これにより、新しいコレクションを見つけたり、新しいコレクションに追加できる人が制限されます。",
"whitelisted_users": "ホワイトリストに登録されたユーザー",
"whitelisted_users_info": "あなたのプロフィールの表示を許可するユーザー名を入力してください。コンマで区切ってください。",
"whitelisted_users_placeholder": "現在、あなたのプロフィールはすべてのユーザーに非表示です...",
"save_changes": "変更を保存",
"import_export": "インポート&エクスポート",
"import_data": "他のプラットフォームからデータをインポート。",
"download_data": "データを即座にダウンロード。",
"export_data": "データをエクスポート",
"delete_account": "アカウントを削除",
"delete_account_warning": "これにより、所有しているすべてのリンク、コレクション、タグ、およびアーカイブデータが完全に削除されます。",
"cancel_subscription_notice": "また、サブスクリプションもキャンセルされます。",
"account_deletion_page": "アカウント削除ページ",
"applying_settings": "設定を適用中...",
"settings_applied": "設定が適用されました!",
"email_change_request": "メール変更リクエストが送信されました。新しいメールアドレスを確認してください。",
"image_upload_no_file_error": "ファイルが選択されていません。アップロードする画像を選んでください。",
"image_upload_size_error": "1MB未満のPNGまたはJPEGファイルを選択してください。",
"image_upload_format_error": "無効なファイル形式です。",
"importing_bookmarks": "ブックマークをインポート中...",
"import_success": "ブックマークがインポートされました!ページを再読み込み中...",
"more_coming_soon": "今後さらに追加予定!",
"billing_settings": "請求設定",
"manage_subscription_intro": "サブスクリプションの管理/キャンセルについては、こちらに訪問してください ",
"billing_portal": "請求ポータル",
"help_contact_intro": "まだ助けが必要な場合や問題が発生した場合は、こちらのアドレスまでお気軽にお問い合わせください:",
"fill_required_fields": "必須項目を入力してください",
"deleting_message": "すべてを削除中、お待ちください...",
"delete_warning": "これにより、所有しているすべてのリンク、コレクション、タグ、およびアーカイブデータが完全に削除され、ログアウトされます。この操作は取り消せません!",
"optional": "任意",
"feedback_help": "(しかし、これは私たちの改善に非常に役立ちます!)",
"reason_for_cancellation": "キャンセルの理由",
"please_specify": "具体的に記載してください",
"customer_service": "カスタマーサービス",
"low_quality": "低品質",
"missing_features": "機能不足",
"switched_service": "サービスを変更した",
"too_complex": "複雑すぎる",
"too_expensive": "高すぎる",
"unused": "未使用",
"other": "その他",
"more_information": "詳細情報 (詳細が多いほど役立ちます)",
"feedback_placeholder": "例: 私が必要だった機能は...",
"delete_your_account": "アカウントを削除",
"change_password": "パスワードを変更",
"password_length_error": "パスワードは8文字以上でなければなりません。",
"applying_changes": "適用中...",
"password_change_instructions": "パスワードを変更するには、以下の項目を入力してください。パスワードは少なくとも8文字以上である必要があります。",
"old_password": "古いパスワード",
"new_password": "新しいパスワード",
"preference": "設定",
"select_theme": "テーマを選択",
"dark": "ダーク",
"light": "ライト",
"archive_settings": "アーカイブ設定",
"formats_to_archive": "ウェブページをアーカイブ/保存する形式:",
"screenshot": "スクリーンショット",
"pdf": "PDF",
"archive_org_snapshot": "Archive.org スナップショット",
"link_settings": "リンク設定",
"prevent_duplicate_links": "重複リンクを防ぐ",
"clicking_on_links_should": "リンクをクリックしたときの動作:",
"open_original_content": "オリジナルコンテンツを開く",
"open_pdf_if_available": "可能な場合はPDFを開く",
"open_readable_if_available": "可能な場合はリーダブルを開く",
"open_screenshot_if_available": "可能な場合はスクリーンショットを開く",
"open_webpage_if_available": "可能な場合はウェブページのコピーを開く",
"tag_renamed": "タグがリネームされました!",
"tag_deleted": "タグが削除されました!",
"rename_tag": "タグをリネーム",
"delete_tag": "タグを削除",
"list_created_with_linkwarden": "Linkwardenで作成されたリスト",
"by_author": "{{author}} による。",
"by_author_and_other": "{{author}} と他 {{count}} 人による。",
"by_author_and_others": "{{author}} と他 {{count}} 人による。",
"search_count_link": "{{count}} リンクを検索",
"search_count_links": "{{count}} リンクを検索",
"collection_is_empty": "このコレクションは空です...",
"all_links": "すべてのリンク",
"all_links_desc": "各コレクションからのリンク",
"you_have_not_added_any_links": "まだリンクを作成していません",
"collections_you_own": "あなたが所有しているコレクション",
"new_collection": "新しいコレクション",
"other_collections": "他のコレクション",
"other_collections_desc": "あなたがメンバーである共有コレクション",
"showing_count_results": "{{count}} 件の結果を表示",
"showing_count_result": "{{count}} 件の結果を表示",
"edit_collection_info": "コレクション情報を編集",
"share_and_collaborate": "共有とコラボレーション",
"view_team": "チームを表示",
"team": "チーム",
"create_subcollection": "サブコレクションを作成",
"delete_collection": "コレクションを削除",
"leave_collection": "コレクションを離れる",
"email_verified_signing_out": "メールが確認されました。サインアウトします...",
"invalid_token": "無効なトークンです。",
"sending_password_recovery_link": "パスワード回復リンクを送信中...",
"please_fill_all_fields": "すべての項目に入力してください。",
"password_updated": "パスワードが更新されました!",
"reset_password": "パスワードをリセット",
"enter_email_for_new_password": "新しいパスワードを作成するためのリンクを送信するために、メールアドレスを入力してください。",
"update_password": "パスワードを更新",
"password_successfully_updated": "パスワードが正常に更新されました。",
"user_already_member": "ユーザーは既に存在します。",
"you_are_already_collection_owner": "あなたは既にコレクションの所有者です。",
"date_newest_first": "日付 (新しい順)",
"date_oldest_first": "日付 (古い順)",
"name_az": "名前 (A-Z)",
"name_za": "名前 (Z-A)",
"description_az": "説明 (A-Z)",
"description_za": "説明 (Z-A)",
"all_rights_reserved": "© {{date}} <0>Linkwarden</0>. All rights reserved.",
"you_have_no_collections": "コレクションがありません...",
"you_have_no_tags": "タグがありません...",
"cant_change_collection_you_dont_own": "所有していないコレクションに変更を加えることはできません。",
"account": "アカウント",
"billing": "請求",
"linkwarden_version": "Linkwarden {{version}}",
"help": "ヘルプ",
"github": "GitHub",
"twitter": "Twitter",
"mastodon": "Mastodon",
"link_preservation_in_queue": "リンク保存がキューに追加されています",
"check_back_later": "後で結果を確認してください",
"there_are_more_formats": "さらに保存されている形式がキューにあります",
"settings": "設定",
"switch_to": "{{theme}} に切り替える",
"logout": "ログアウト",
"start_journey": "新しいリンクを作成して、旅を始めましょう!",
"create_new_link": "新しいリンクを作成",
"new_link": "新しいリンク",
"create_new": "新しく作成...",
"pwa_install_prompt": "ホーム画面にLinkwardenをインストールして、より高速で快適な体験を。 <0>詳しくはこちら</0>",
"full_content": "全コンテンツ",
"slower": "遅い",
"new_version_announcement": "新しいバージョンの <0>Linkwarden {{version}}</0> をご覧ください!",
"creating": "作成中...",
"upload_file": "ファイルをアップロード",
"file": "ファイル",
"file_types": "PDF, PNG, JPG (最大 {{size}} MB)",
"description": "説明",
"auto_generated": "何も入力されない場合、自動生成されます。",
"example_link": "例: サンプルリンク",
"hide": "隠す",
"more": "詳細",
"options": "オプション",
"description_placeholder": "メモ、考えなど",
"deleting": "削除中...",
"token_revoked": "トークンが無効化されました。",
"revoke_token": "トークンを無効化",
"revoke_confirmation": "このアクセス トークンを無効化してもよろしいですかこのトークンを使用しているアプリやサービスは、Linkwardenにアクセスできなくなります。",
"revoke": "無効化",
"sending_request": "リクエストを送信中...",
"link_being_archived": "リンクがアーカイブされています...",
"preserved_formats": "保存された形式",
"available_formats": "このリンクには以下の形式が利用可能です",
"readable": "リーダブル",
"preservation_in_queue": "リンクの保存がキューに追加されています",
"view_latest_snapshot": "archive.org で最新のスナップショットを見る",
"refresh_preserved_formats": "保存形式を更新",
"this_deletes_current_preservations": "これにより現在の保存データが削除されます",
"create_new_user": "新しいユーザーを作成",
"placeholder_johnny": "ジョニー",
"placeholder_email": "johnny@example.com",
"placeholder_john": "ジョン",
"user_created": "ユーザーが作成されました!",
"fill_all_fields_error": "すべての項目に入力してください。",
"password_change_note": "<0>注意:</0> パスワードを変更する必要があることをユーザーに知らせてください。",
"create_user": "ユーザーを作成",
"creating_token": "トークンを作成中...",
"token_created": "トークンが作成されました!",
"access_token_created": "アクセス トークンが作成されました",
"token_creation_notice": "新しいトークンが作成されました。コピーして安全な場所に保管してください。再度表示することはできません。",
"copied_to_clipboard": "クリップボードにコピーされました!",
"copy_to_clipboard": "クリップボードにコピー",
"create_access_token": "アクセス トークンを作成",
"expires_in": "有効期限",
"token_name_placeholder": "例: iOSショートカット用",
"create_token": "アクセス トークンを作成",
"7_days": "7 日間",
"30_days": "30 日間",
"60_days": "60 日間",
"90_days": "90 日間",
"no_expiration": "有効期限なし",
"creating_link": "リンクを作成中...",
"link_created": "リンクが作成されました!",
"link_name_placeholder": "空白のままにすると自動生成されます。",
"link_url_placeholder": "例: http://example.com/",
"link_description_placeholder": "メモ、考えなど",
"more_options": "詳細オプション",
"hide_options": "オプションを隠す",
"create_link": "リンクを作成",
"new_sub_collection": "新しいサブコレクション",
"for_collection": "{{name}} 用",
"create_new_collection": "新しいコレクションを作成",
"color": "カラー",
"reset_defaults": "デフォルトにリセット",
"updating_collection": "コレクションを更新中...",
"collection_name_placeholder": "例: サンプルコレクション",
"collection_description_placeholder": "このコレクションの目的...",
"create_collection_button": "コレクションを作成",
"password_change_warning": "メールアドレスを変更する前にパスワードを確認してください。",
"stripe_update_note": " このフィールドを更新すると、Stripe上の請求先メールも変更されます。",
"sso_will_be_removed_warning": "メールアドレスを変更すると、既存の {{service}} SSO接続が解除されます。",
"old_email": "旧メールアドレス",
"new_email": "新しいメールアドレス",
"confirm": "確認",
"edit_link": "リンクを編集",
"updating": "更新中...",
"updated": "更新されました!",
"placeholder_example_link": "例: サンプルリンク",
"make_collection_public": "コレクションを公開",
"make_collection_public_checkbox": "このコレクションを公開する",
"make_collection_public_desc": "これにより、誰でもこのコレクションとそのユーザーを閲覧できるようになります。",
"sharable_link_guide": "共有リンク (クリックしてコピー)",
"copied": "コピーされました!",
"members": "メンバー",
"members_username_placeholder": "ユーザー名 ('@' を含まない)",
"owner": "所有者",
"admin": "管理者",
"contributor": "コントリビューター",
"viewer": "閲覧者",
"viewer_desc": "読み取り専用アクセス",
"contributor_desc": "リンクを閲覧および作成可能",
"admin_desc": "すべてのリンクにフルアクセス",
"remove_member": "メンバーを削除",
"placeholder_example_collection": "例: サンプルコレクション",
"placeholder_collection_purpose": "このコレクションの目的...",
"deleting_user": "削除中...",
"user_deleted": "ユーザーが削除されました。",
"delete_user": "ユーザーを削除",
"confirm_user_deletion": "このユーザーを削除してもよろしいですか?",
"irreversible_action_warning": "この操作は取り消せません!",
"delete_confirmation": "削除する、私はこの操作を理解しています",
"delete_link": "リンクを削除",
"deleted": "削除されました。",
"link_deletion_confirmation_message": "このリンクを削除してもよろしいですか?",
"warning": "警告",
"irreversible_warning": "この操作は取り消せません!",
"shift_key_tip": "Shiftキーを押しながら「削除」をクリックすると、今後この確認をスキップできます。",
"deleting_collection": "削除中...",
"collection_deleted": "コレクションが削除されました。",
"confirm_deletion_prompt": "確認のため、以下のボックスに \"{{name}}\" と入力してください:",
"type_name_placeholder": "ここに \"{{name}}\" を入力してください。",
"deletion_warning": "このコレクションを削除すると、そのすべての内容が永久に消去され、以前アクセス可能だったメンバーを含め、誰もアクセスできなくなります。",
"leave_prompt": "現在のコレクションから離れるには、以下のボタンをクリックしてください。",
"leave": "離れる",
"edit_links": "{{count}} リンクを編集",
"move_to_collection": "コレクションに移動",
"add_tags": "タグを追加",
"remove_previous_tags": "以前のタグを削除",
"delete_links": "{{count}} リンクを削除",
"links_deletion_confirmation_message": "{{count}} リンクを削除してもよろしいですか? ",
"warning_irreversible": "警告:この操作は取り消せません!",
"shift_key_instruction": "Shiftキーを押しながら「削除」をクリックすると、今後この確認をスキップできます。",
"link_selection_error": "この項目を編集または削除する権限がありません。",
"no_description": "説明が提供されていません。",
"applying": "適用中...",
"unpin": "ピンを外す",
"pin_to_dashboard": "ダッシュボードにピン留め",
"show_link_details": "リンクの詳細を表示",
"link_pinned": "リンクがピン留めされました!",
"link_unpinned": "リンクのピンが外れました!",
"webpage": "ウェブページ",
"server_administration": "サーバー管理",
"all_collections": "すべてのコレクション",
"dashboard": "ダッシュボード",
"demo_title": "デモ専用",
"demo_desc": "これはLinkwardenのデモインスタンスであり、アップロードは無効になっています。",
"demo_desc_2": "完全版を試したい場合は、以下で無料トライアルにサインアップできます:",
"demo_button": "デモユーザーとしてログイン",
"regular": "通常",
"thin": "細字",
"bold": "太字",
"fill": "塗りつぶし",
"duotone": "デュオトーン2色",
"light_icon": "軽量",
"search": "検索",
"set_custom_icon": "カスタムアイコンを設定",
"view": "閲覧",
"show": "表示",
"image": "画像",
"icon": "アイコン",
"date": "日付",
"preview_unavailable": "プレビューが利用できません",
"saved": "保存済み",
"untitled": "無題",
"no_tags": "タグなし",
"no_description_provided": "説明が提供されていません",
"change_icon": "アイコンを変更",
"upload_preview_image": "プレビュー画像をアップロード",
"columns": "列",
"default": "デフォルト"
}

View File

@@ -0,0 +1,375 @@
{
"user_administration": "Gebruikersbeheer",
"search_users": "Zoeken naar Gebruikers",
"no_users_found": "Geen gebruikers gevonden.",
"no_user_found_in_search": "Geen gebruikers gevonden met de gegeven zoekopdracht.",
"username": "Gebruikersnaam",
"email": "E-mail",
"subscribed": "Geabonneerd",
"created_at": "Aangemaakt Op",
"not_available": "N.V.T.",
"check_your_email": "Controleer uw E-mail",
"authenticating": "Authenticatie...",
"verification_email_sent": "Verificatie-e-mail verzonden.",
"verification_email_sent_desc": "Een inloglink is naar uw e-mailadres verzonden. Als u de e-mail niet ziet, controleer dan uw spammap.",
"resend_email": "E-mail Opnieuw Verzenden",
"invalid_credentials": "Ongeldige inloggegevens.",
"fill_all_fields": "Vul alle velden in.",
"enter_credentials": "Voer uw inloggegevens in",
"username_or_email": "Gebruikersnaam of E-mail",
"password": "Wachtwoord",
"confirm_password": "Bevestig Wachtwoord",
"forgot_password": "Wachtwoord Vergeten?",
"login": "Inloggen",
"or_continue_with": "Of ga verder met",
"new_here": "Nieuw hier?",
"sign_up": "Aanmelden",
"sign_in_to_your_account": "Log in op uw account",
"dashboard_desc": "Een kort overzicht van uw gegevens",
"link": "Link",
"links": "Links",
"collection": "Collectie",
"collections": "Collecties",
"tag": "Tag",
"tags": "Tags",
"recent": "Recent",
"recent_links_desc": "Recent Toegevoegde Links",
"view_all": "Bekijk Alles",
"view_added_links_here": "Bekijk Hier Uw Recent Toegevoegde Links!",
"view_added_links_here_desc": "Deze sectie toont uw laatste toegevoegde links in alle collecties waartoe u toegang hebt.",
"add_link": "Nieuwe Link Toevoegen",
"import_links": "Links Importeren",
"from_linkwarden": "Van Linkwarden",
"from_html": "Van Bladwijzers HTML-bestand",
"from_wallabag": "Van Wallabag (JSON-bestand)",
"pinned": "Vastgemaakt",
"pinned_links_desc": "Uw Vastgemaakte Links",
"pin_favorite_links_here": "Maak Hier Uw Favoriete Links Vast!",
"pin_favorite_links_here_desc": "U kunt uw favoriete links vastmaken door op de drie stippen op elke link te klikken en Vastmaken aan Dashboard te selecteren.",
"sending_password_link": "Wachtwoordherstel link verzenden...",
"password_email_prompt": "Voer uw e-mailadres in zodat we u een link kunnen sturen om een nieuw wachtwoord aan te maken.",
"send_reset_link": "Herstel Link Versturen",
"reset_email_sent_desc": "Controleer uw e-mail voor een link om uw wachtwoord te resetten. Als u de e-mail niet binnen enkele minuten ontvangt, controleer dan uw spammap.",
"back_to_login": "Terug naar Inloggen",
"email_sent": "E-mail Verzonden!",
"passwords_mismatch": "Wachtwoorden komen niet overeen.",
"password_too_short": "Wachtwoorden moeten minstens 8 tekens bevatten.",
"creating_account": "Account Aanmaken...",
"account_created": "Account Aangemaakt!",
"trial_offer_desc": "Ontgrendel {{count}} dagen Premium Service zonder kosten!",
"register_desc": "Maak een nieuw account aan",
"registration_disabled_desc": "Registratie is uitgeschakeld voor deze instantie, neem contact op met de beheerder in geval van problemen.",
"enter_details": "Voer uw gegevens in",
"display_name": "Weergavenaam",
"sign_up_agreement": "Door u aan te melden, gaat u akkoord met onze <0>Gebruiksvoorwaarden</0> en <1>Privacybeleid</1>.",
"need_help": "Hulp nodig?",
"get_in_touch": "Neem contact op",
"already_registered": "Heeft u al een account?",
"deleting_selections": "Selecties Verwijderen...",
"links_deleted": "{{count}} Links verwijderd.",
"link_deleted": "1 Link verwijderd.",
"links_selected": "{{count}} Links geselecteerd",
"link_selected": "1 Link geselecteerd",
"nothing_selected": "Niets geselecteerd",
"edit": "Bewerken",
"delete": "Verwijderen",
"nothing_found": "Niets gevonden.",
"redirecting_to_stripe": "Doorverwijzen naar Stripe...",
"subscribe_title": "Abonneer u op Linkwarden!",
"subscribe_desc": "U wordt doorgestuurd naar Stripe, neem gerust contact met ons op via <0>support@linkwarden.app</0> in geval van problemen.",
"monthly": "Maandelijks",
"yearly": "Jaarlijks",
"discount_percent": "{{percent}}% Korting",
"billed_monthly": "Maandelijks Gefactureerd",
"billed_yearly": "Jaarlijks Gefactureerd",
"total": "Totaal",
"total_annual_desc": "{{count}}-dagen gratis proefperiode, daarna ${{annualPrice}} per jaar",
"total_monthly_desc": "{{count}}-dagen gratis proefperiode, daarna ${{monthlyPrice}} per maand",
"plus_tax": "+ BTW indien van toepassing",
"complete_subscription": "Abonnement Afronden",
"sign_out": "Uitloggen",
"access_tokens": "Toegangstokens",
"access_tokens_description": "Toegangstokens kunnen worden gebruikt om toegang te krijgen tot Linkwarden vanuit andere apps en services zonder uw gebruikersnaam en wachtwoord te verstrekken.",
"new_token": "Nieuw Toegangstoken",
"name": "Naam",
"created_success": "Aangemaakt!",
"created": "Aangemaakt",
"expires": "Verloopt",
"accountSettings": "Accountinstellingen",
"language": "Taal",
"profile_photo": "Profielfoto",
"upload_new_photo": "Upload een nieuwe foto...",
"remove_photo": "Foto Verwijderen",
"make_profile_private": "Maak profiel privé",
"profile_privacy_info": "Dit beperkt wie u kan vinden en toevoegen aan nieuwe collecties.",
"whitelisted_users": "Toegestane Gebruikers",
"whitelisted_users_info": "Geef alstublieft de gebruikersnaam op van de gebruikers die u zichtbaarheid wilt geven op uw profiel. Gescheiden door komma's.",
"whitelisted_users_placeholder": "Uw profiel is momenteel verborgen voor iedereen...",
"save_changes": "Wijzigingen Opslaan",
"import_export": "Importeren & Exporteren",
"import_data": "Importeer uw gegevens van andere platforms.",
"download_data": "Download uw gegevens direct.",
"export_data": "Gegevens Exporteren",
"delete_account": "Account Verwijderen",
"delete_account_warning": "Dit zal PERMANENT alle links, collecties, tags en gearchiveerde gegevens die u bezit verwijderen.",
"cancel_subscription_notice": "Dit zal ook uw abonnement annuleren.",
"account_deletion_page": "Accountverwijdering pagina",
"applying_settings": "Instellingen Toepassen...",
"settings_applied": "Instellingen Toegepast!",
"email_change_request": "E-mailwijzigingsverzoek verzonden. Verifieer het nieuwe e-mailadres.",
"image_upload_size_error": "Selecteer een PNG- of JPEG-bestand dat kleiner is dan 1 MB.",
"image_upload_format_error": "Ongeldig bestandsformaat.",
"importing_bookmarks": "Bladwijzers importeren...",
"import_success": "Bladwijzers geïmporteerd! Pagina opnieuw laden...",
"more_coming_soon": "Meer binnenkort beschikbaar!",
"billing_settings": "Factureringsinstellingen",
"manage_subscription_intro": "Beheer of annuleer uw abonnement door naar de",
"billing_portal": "Factureringsportaal",
"help_contact_intro": "Als u nog steeds hulp nodig heeft of problemen ondervindt, neem gerust contact met ons op via:",
"fill_required_fields": "Vul de verplichte velden in.",
"deleting_message": "Alles verwijderen, even geduld aub...",
"delete_warning": "Dit zal PERMANENT alle links, collecties, tags en gearchiveerde gegevens die u bezit verwijderen. Dit zal ook leiden tot uitloggen. Deze actie is onomkeerbaar!",
"optional": "Optioneel",
"feedback_help": "(maar het helpt ons echt om te verbeteren!)",
"reason_for_cancellation": "Reden voor annulering",
"please_specify": "Specificeer alstublieft",
"customer_service": "Klantenservice",
"low_quality": "Lage Kwaliteit",
"missing_features": "Ontbrekende Functies",
"switched_service": "Overgestapt naar andere dienst",
"too_complex": "Te Complex",
"too_expensive": "Te Duur",
"unused": "Ongebruikt",
"other": "Anders",
"more_information": "Meer informatie (hoe meer details, hoe nuttiger het zou zijn)",
"feedback_placeholder": "bijv. Ik had een functie nodig die...",
"delete_your_account": "Verwijder Uw Account",
"change_password": "Wachtwoord Wijzigen",
"password_length_error": "Wachtwoorden moeten minstens 8 tekens bevatten.",
"applying_changes": "Wijzigingen Toepassen...",
"password_change_instructions": "Om uw wachtwoord te wijzigen, vul alstublieft het volgende in. Uw wachtwoord moet minimaal 8 tekens bevatten.",
"old_password": "Oud Wachtwoord",
"new_password": "Nieuw Wachtwoord",
"preference": "Voorkeur",
"select_theme": "Selecteer Thema",
"dark": "Donker",
"light": "Licht",
"archive_settings": "Archiveringsinstellingen",
"formats_to_archive": "Te archiveren/behouden formaten voor webpagina's:",
"screenshot": "Screenshot",
"pdf": "PDF",
"archive_org_snapshot": "Archive.org Momentopname",
"link_settings": "Linkinstellingen",
"prevent_duplicate_links": "Voorkom dubbele links",
"clicking_on_links_should": "Klikken op links moet:",
"open_original_content": "Oorspronkelijke inhoud openen",
"open_pdf_if_available": "PDF openen, indien beschikbaar",
"open_readable_if_available": "Leesbare versie openen, indien beschikbaar",
"open_screenshot_if_available": "Screenshot openen, indien beschikbaar",
"open_webpage_if_available": "Webpagina kopie openen, indien beschikbaar",
"tag_renamed": "Tag hernoemd!",
"tag_deleted": "Tag verwijderd!",
"rename_tag": "Tag Hernoemen",
"delete_tag": "Tag Verwijderen",
"list_created_with_linkwarden": "Lijst gemaakt met Linkwarden",
"by_author": "Door {{author}}.",
"by_author_and_other": "Door {{author}} en {{count}} andere.",
"by_author_and_others": "Door {{author}} en {{count}} anderen.",
"search_count_link": "Zoek {{count}} Link",
"search_count_links": "Zoek {{count}} Links",
"collection_is_empty": "Deze Collectie is leeg...",
"all_links": "Alle Links",
"all_links_desc": "Links uit elke Collectie",
"you_have_not_added_any_links": "U Heeft Nog Geen Links Aangemaakt",
"collections_you_own": "Collecties die u bezit",
"new_collection": "Nieuwe Collectie",
"other_collections": "Andere Collecties",
"other_collections_desc": "Gedeelde collecties waarvan u lid bent",
"showing_count_results": "Toont {{count}} resultaten",
"showing_count_result": "Toont {{count}} resultaat",
"edit_collection_info": "Collectie Info Bewerken",
"share_and_collaborate": "Delen en Samenwerken",
"view_team": "Bekijk Team",
"team": "Team",
"create_subcollection": "Sub-Collectie Aanmaken",
"delete_collection": "Collectie Verwijderen",
"leave_collection": "Collectie Verlaten",
"email_verified_signing_out": "E-mail geverifieerd. Uitloggen...",
"invalid_token": "Ongeldig token.",
"sending_password_recovery_link": "Wachtwoordherstel link verzenden...",
"please_fill_all_fields": "Vul alle velden in.",
"password_updated": "Wachtwoord Bijgewerkt!",
"reset_password": "Wachtwoord Resetten",
"enter_email_for_new_password": "Voer uw e-mail in zodat we u een link kunnen sturen om een nieuw wachtwoord aan te maken.",
"update_password": "Wachtwoord Bijwerken",
"password_successfully_updated": "Uw wachtwoord is succesvol bijgewerkt.",
"user_already_member": "Gebruiker bestaat al.",
"you_are_already_collection_owner": "U bent al de eigenaar van de collectie.",
"date_newest_first": "Datum (Nieuwste Eerst)",
"date_oldest_first": "Datum (Oudste Eerst)",
"name_az": "Naam (A-Z)",
"name_za": "Naam (Z-A)",
"description_az": "Beschrijving (A-Z)",
"description_za": "Beschrijving (Z-A)",
"all_rights_reserved": "© {{date}} <0>Linkwarden</0>. Alle rechten voorbehouden.",
"you_have_no_collections": "U heeft geen Collecties...",
"you_have_no_tags": "U heeft geen Tags...",
"cant_change_collection_you_dont_own": "U kunt geen wijzigingen aanbrengen in een collectie die u niet bezit.",
"account": "Account",
"billing": "Facturering",
"linkwarden_version": "Linkwarden {{version}}",
"help": "Hulp",
"github": "GitHub",
"twitter": "Twitter",
"mastodon": "Mastodon",
"link_preservation_in_queue": "De Linkbewaring staat momenteel in de wachtrij",
"check_back_later": "Kom later terug om het resultaat te bekijken",
"there_are_more_formats": "Er zijn meer bewaarde formaten in de wachtrij",
"settings": "Instellingen",
"switch_to": "Schakel over naar {{theme}}",
"logout": "Uitloggen",
"start_journey": "Begin uw reis door een nieuwe link aan te maken!",
"create_new_link": "Nieuwe Link Aanmaken",
"new_link": "Nieuwe Link",
"create_new": "Nieuwe Aanmaken...",
"pwa_install_prompt": "Installeer Linkwarden op uw startscherm voor snellere toegang en een verbeterde ervaring. <0>Meer leren</0>",
"full_content": "Volledige Inhoud",
"slower": "Langzamer",
"new_version_announcement": "Zie wat er nieuw is in <0>Linkwarden {{version}}!</0>",
"creating": "Aanmaken...",
"upload_file": "Bestand Uploaden",
"file": "Bestand",
"file_types": "PDF, PNG, JPG (tot {{size}} MB)",
"description": "Beschrijving",
"auto_generated": "Wordt automatisch gegenereerd als er niets wordt opgegeven.",
"example_link": "bijv. Voorbeeldlink",
"hide": "Verbergen",
"more": "Meer",
"options": "Opties",
"description_placeholder": "Notities, gedachten, enz.",
"deleting": "Verwijderen...",
"token_revoked": "Token Ingetrokken.",
"revoke_token": "Token Intrekken",
"revoke_confirmation": "Weet u zeker dat u dit toegangstoken wilt intrekken? Apps of services die dit token gebruiken, zullen Linkwarden niet langer kunnen benaderen.",
"revoke": "Intrekken",
"sending_request": "Verzoek Verzenden...",
"link_being_archived": "Link wordt gearchiveerd...",
"preserved_formats": "Bewaarde Formaten",
"available_formats": "De volgende formaten zijn beschikbaar voor deze link",
"readable": "Leesbaar",
"preservation_in_queue": "Linkbewaring staat in de wachtrij",
"view_latest_snapshot": "Bekijk de laatste momentopname op archive.org",
"refresh_preserved_formats": "Vernieuw Bewaarde Formaten",
"this_deletes_current_preservations": "Dit verwijdert de huidige bewaarmethodes",
"create_new_user": "Nieuwe Gebruiker Aanmaken",
"placeholder_johnny": "Johnny",
"placeholder_email": "johnny@voorbeeld.com",
"placeholder_john": "john",
"user_created": "Gebruiker Aangemaakt!",
"fill_all_fields_error": "Vul alle velden in.",
"password_change_note": "<0>Opmerking:</0> Zorg ervoor dat u de gebruiker informeert dat ze hun wachtwoord moeten wijzigen.",
"create_user": "Gebruiker Aanmaken",
"creating_token": "Token Aanmaken...",
"token_created": "Token Aangemaakt!",
"access_token_created": "Toegangstoken Aangemaakt",
"token_creation_notice": "Uw nieuwe token is aangemaakt. Kopieer het en bewaar het ergens veilig. U kunt het niet opnieuw bekijken.",
"copied_to_clipboard": "Gekopieerd naar klembord!",
"copy_to_clipboard": "Kopiëren naar Klembord",
"create_access_token": "Toegangstoken Aanmaken",
"expires_in": "Verloopt over",
"token_name_placeholder": "bijv. Voor de iOS snelkoppeling",
"create_token": "Toegangstoken Aanmaken",
"7_days": "7 Dagen",
"30_days": "30 Dagen",
"60_days": "60 Dagen",
"90_days": "90 Dagen",
"no_expiration": "Geen Verloopdatum",
"creating_link": "Link Aanmaken...",
"link_created": "Link Aangemaakt!",
"link_name_placeholder": "Wordt automatisch gegenereerd als leeg gelaten.",
"link_url_placeholder": "bijv. http://voorbeeld.com/",
"link_description_placeholder": "Notities, gedachten, enz.",
"more_options": "Meer Opties",
"hide_options": "Opties Verbergen",
"create_link": "Link Aanmaken",
"new_sub_collection": "Nieuwe Sub-Collectie",
"for_collection": "Voor {{name}}",
"create_new_collection": "Nieuwe Collectie Aanmaken",
"color": "Kleur",
"reset": "Herstellen",
"updating_collection": "Collectie Bijwerken...",
"collection_name_placeholder": "bijv. Voorbeeld Collectie",
"collection_description_placeholder": "Het doel van deze Collectie...",
"create_collection_button": "Collectie Aanmaken",
"password_change_warning": "Bevestig uw wachtwoord voordat u uw e-mailadres wijzigt.",
"stripe_update_note": "Door dit veld bij te werken, wijzigt u ook uw facturerings-e-mail op Stripe.",
"sso_will_be_removed_warning": "Als u uw e-mailadres wijzigt, worden alle bestaande {{service}} SSO-verbindingen verwijderd.",
"old_email": "Oud E-mailadres",
"new_email": "Nieuw E-mailadres",
"confirm": "Bevestigen",
"edit_link": "Link Bewerken",
"updating": "Bijwerken...",
"updated": "Bijgewerkt!",
"placeholder_example_link": "bijv. Voorbeeld Link",
"make_collection_public": "Maak Collectie Openbaar",
"make_collection_public_checkbox": "Maak dit een openbare collectie",
"make_collection_public_desc": "Hierdoor kan iedereen deze collectie en zijn gebruikers bekijken.",
"sharable_link_guide": "Deelbare Link (Klik om te kopiëren)",
"copied": "Gekopieerd!",
"members": "Leden",
"members_username_placeholder": "Gebruikersnaam (zonder '@')",
"owner": "Eigenaar",
"admin": "Beheerder",
"contributor": "Bijdrager",
"viewer": "Lezer",
"viewer_desc": "Alleen-lezen toegang",
"contributor_desc": "Kan links bekijken en aanmaken",
"admin_desc": "Volledige toegang tot alle links",
"remove_member": "Lid Verwijderen",
"placeholder_example_collection": "bijv. Voorbeeld Collectie",
"placeholder_collection_purpose": "Het doel van deze Collectie...",
"deleting_user": "Verwijderen...",
"user_deleted": "Gebruiker Verwijderd.",
"delete_user": "Gebruiker Verwijderen",
"confirm_user_deletion": "Weet u zeker dat u deze gebruiker wilt verwijderen?",
"irreversible_action_warning": "Deze actie is onomkeerbaar!",
"delete_confirmation": "Verwijderen, ik weet wat ik doe",
"delete_link": "Link Verwijderen",
"deleted": "Verwijderd.",
"link_deletion_confirmation_message": "Weet u zeker dat u deze link wilt verwijderen?",
"warning": "Waarschuwing",
"irreversible_warning": "Deze actie is onomkeerbaar!",
"shift_key_tip": "Houd de Shift-toets ingedrukt terwijl u op 'Verwijderen' klikt om deze bevestiging in de toekomst te omzeilen.",
"deleting_collection": "Verwijderen...",
"collection_deleted": "Collectie Verwijderd.",
"confirm_deletion_prompt": "Ter bevestiging, typ \"{{name}}\" in het onderstaande vak:",
"type_name_placeholder": "Typ Hier \"{{name}}\".",
"deletion_warning": "Als u deze collectie verwijdert, worden alle inhoud permanent gewist, en wordt deze ontoegankelijk voor iedereen, inclusief leden met eerdere toegang.",
"leave_prompt": "Klik op de onderstaande knop om de huidige collectie te verlaten.",
"leave": "Verlaten",
"edit_links": "Bewerk {{count}} Links",
"move_to_collection": "Verplaatsen naar Collectie",
"add_tags": "Tags Toevoegen",
"remove_previous_tags": "Verwijder vorige tags",
"delete_links": "Verwijder {{count}} Links",
"links_deletion_confirmation_message": "Weet u zeker dat u {{count}} links wilt verwijderen?",
"warning_irreversible": "Waarschuwing: Deze actie is onomkeerbaar!",
"shift_key_instruction": "Houd de 'Shift'-toets ingedrukt terwijl u op 'Verwijderen' klikt om deze bevestiging in de toekomst te omzeilen.",
"link_selection_error": "U heeft geen toestemming om dit item te bewerken of te verwijderen.",
"no_description": "Geen beschrijving opgegeven.",
"applying": "Toepassen...",
"unpin": "Losmaken",
"pin_to_dashboard": "Vastmaken aan Dashboard",
"show_link_details": "Toon Linkdetails",
"hide_link_details": "Verberg Linkdetails",
"link_pinned": "Link Vastgemaakt!",
"link_unpinned": "Link Losgemaakt!",
"webpage": "Webpagina",
"server_administration": "Serverbeheer",
"all_collections": "Alle Collecties",
"dashboard": "Dashboard",
"demo_title": "Demo Alleen",
"demo_desc": "Dit is slechts een demo-instantie van Linkwarden en uploads zijn uitgeschakeld.",
"demo_desc_2": "Als u de volledige versie wilt proberen, kunt u zich aanmelden voor een gratis proefperiode op:",
"demo_button": "Inloggen als demo gebruiker"
}

View File

@@ -0,0 +1,400 @@
{
"user_administration": "Administração de Usuários",
"search_users": "Pesquisar por Usuários",
"no_users_found": "Nenhum usuário encontrado.",
"no_user_found_in_search": "Nenhum usuário encontrado com a consulta de pesquisa fornecida.",
"username": "Nome de Usuário",
"email": "E-mail",
"subscribed": "Inscrito",
"created_at": "Criado Em",
"not_available": "N/D",
"check_your_email": "Por favor, verifique seu E-mail",
"authenticating": "Autenticando...",
"verification_email_sent": "E-mail de verificação enviado.",
"view": "Visualizar",
"verification_email_sent_desc": "Um link de login foi enviado para seu endereço de e-mail. Se você não vir o e-mail, verifique sua pasta de spam.",
"resend_email": "Reenviar E-mail",
"reset_defaults": "redefinir para o Padrão",
"invalid_credentials": "Credenciais inválidas.",
"fill_all_fields": "Por favor, preencha todos os campos.",
"enter_credentials": "Digite suas credenciais",
"username_or_email": "Nome de Usuário ou E-mail",
"password": "Senha",
"confirm_password": "Confirmar Senha",
"forgot_password": "Esqueceu a Senha?",
"login": "Login",
"or_continue_with": "Ou continue com",
"new_here": "Novo por aqui?",
"sign_up": "Inscrever-se",
"sign_in_to_your_account": "Faça login na sua conta",
"dashboard_desc": "Uma breve visão geral dos seus dados",
"date": "Data",
"link": "Link",
"links": "Links",
"collection": "Coleção",
"collections": "Coleções",
"tag": "Tag",
"tags": "Tags",
"recent": "Recentes",
"recent_links_desc": "Links adicionados recentemente",
"view_all": "Ver Todos",
"view_added_links_here": "Veja Seus Links Adicionados Recentemente Aqui!",
"view_added_links_here_desc": "Esta seção exibirá seus Links adicionados mais recentemente em todas as Coleções às quais você tem acesso.",
"add_link": "Adicionar Novo Link",
"import_links": "Importar Links",
"from_linkwarden": "Do Linkwarden",
"from_html": "Do arquivo HTML de Favoritos",
"from_wallabag": "Do Wallabag (arquivo JSON)",
"pinned": "Fixados",
"pinned_links_desc": "Seus Links fixados",
"pin_favorite_links_here": "Fixe Seus Links Favoritos Aqui!",
"pin_favorite_links_here_desc": "Você pode Fixar seus Links favoritos clicando nos três pontos em cada Link e clicando em Fixar no Painel.",
"sending_password_link": "Enviando link de recuperação de senha...",
"password_email_prompt": "Digite seu e-mail para que possamos enviar um link para criar uma nova senha.",
"send_reset_link": "Enviar Link de Redefinição",
"reset_email_sent_desc": "Verifique seu e-mail para obter um link para redefinir sua senha. Se não aparecer em alguns minutos, verifique sua pasta 1 de spam.",
"back_to_login": "Voltar para o Login",
"email_sent": "E-mail Enviado!",
"passwords_mismatch": "As senhas não coincidem.",
"password_too_short": "As senhas devem ter pelo menos 8 caracteres.",
"creating_account": "Criando Conta...",
"account_created": "Conta Criada!",
"trial_offer_desc": "Desbloqueie {{count}} dias de Serviço Premium sem custo!",
"register_desc": "Criar uma nova conta",
"registration_disabled_desc": "O registro está desativado para esta instância, entre em contato com o administrador em caso de problemas.",
"regular": "Regular",
"enter_details": "Digite seus dados",
"display_name": "Nome de Exibição",
"sign_up_agreement": "Ao se inscrever, você concorda com nossos Termos de Serviço 1 e Política de Privacidade.",
"need_help": "Precisa de ajuda?",
"get_in_touch": "Entre em contato",
"already_registered": "Já tem uma conta?",
"deleting_selections": "Excluindo seleções...",
"links_deleted": "{{count}} Links excluídos.",
"link_deleted": "1 Link excluído.",
"links_selected": "{{count}} Links selecionados",
"link_selected": "1 Link selecionado",
"nothing_selected": "Nada selecionado",
"edit": "Editar",
"delete": "Excluir",
"nothing_found": "Nada encontrado.",
"redirecting_to_stripe": "Redirecionando para o Stripe...",
"subscribe_title": "Assine o Linkwarden!",
"subscribe_desc": "Você será redirecionado para o Stripe, sinta-se à vontade para nos contatar em support@linkwarden.app 1 em caso de qualquer problema.",
"monthly": "Mensal",
"yearly": "Anual",
"discount_percent": "{{percent}}% de Desconto",
"billed_monthly": "Cobrado Mensalmente",
"billed_yearly": "Cobrado Anualmente",
"total": "Total",
"total_annual_desc": "Teste grátis de {{count}} dias, depois ${{annualPrice}} anualmente",
"total_monthly_desc": "Teste grátis de {{count}} dias, depois ${{monthlyPrice}} por mês",
"plus_tax": "+ IVA se aplicável",
"complete_subscription": "Concluir Assinatura",
"sign_out": "Sair",
"access_tokens": "Tokens de Acesso",
"access_tokens_description": "Os Tokens de Acesso podem ser usados para acessar o Linkwarden de outros aplicativos e serviços sem fornecer seu Nome de Usuário e Senha.",
"new_token": "Novo Token de Acesso",
"name": "Nome",
"created_success": "Criado!",
"created": "Criado",
"expires": "Expira",
"accountSettings": "Configurações da Conta",
"language": "Idioma",
"profile_photo": "Foto de Perfil",
"upload_new_photo": "Carregar uma nova foto...",
"upload_preview_image": "Carregar Imagem de Pré-visualização",
"remove_photo": "Remover Foto",
"make_profile_private": "Tornar o perfil privado",
"profile_privacy_info": "Isso limitará quem pode encontrar e adicionar você a novas Coleções.",
"whitelisted_users": "Usuários da Lista de Permissões",
"whitelisted_users_info": "Forneça o Nome de Usuário dos usuários aos quais você deseja conceder visibilidade ao seu perfil. Separados por vírgula.",
"whitelisted_users_placeholder": "Seu perfil está oculto de todos agora...",
"save_changes": "Salvar Alterações",
"saved": "Salvo",
"import_export": "Importar e Exportar",
"import_data": "Importe seus dados de outras plataformas.",
"download_data": "Baixe seus dados instantaneamente.",
"duotone": "Duotone",
"export_data": "Exportar Dados",
"delete_account": "Excluir Conta",
"delete_account_warning": "Isso excluirá permanentemente TODOS os Links, Coleções, Tags e dados arquivados que você possui.",
"cancel_subscription_notice": "Também cancelará sua assinatura.",
"account_deletion_page": "Página de exclusão de conta",
"applying_settings": "Aplicando configurações...",
"settings_applied": "Configurações Aplicadas!",
"sharable_link": "Link Compartilhável",
"email_change_request": "Solicitação de alteração de e-mail enviada. Por favor, verifique o novo endereço de e-mail.",
"image_upload_no_file_error": "Nenhum arquivo selecionado. Por favor, escolha uma imagem para carregar.",
"image_upload_size_error": "Por favor, selecione um arquivo PNG ou JPEG com menos de 1MB.",
"image_upload_format_error": "Formato de arquivo inválido.",
"importing_bookmarks": "Importando favoritos...",
"import_success": "Favoritos importados! Recarregando a página...",
"more_coming_soon": "Mais em breve!",
"billing_settings": "Configurações de Cobrança",
"bold": "Negrito",
"manage_subscription_intro": "Para gerenciar/cancelar sua assinatura, visite o",
"billing_portal": "Portal de Faturamento",
"help_contact_intro": "Se você ainda precisar de ajuda ou encontrar algum problema, sinta-se à vontade para nos contatar em:",
"fill_required_fields": "Por favor, preencha os campos obrigatórios.",
"deleting_message": "Excluindo tudo, por favor aguarde...",
"delete_warning": "Isso excluirá permanentemente todos os Links, Coleções, Tags e dados arquivados que você possui. Também irá desconectá-lo. Esta ação é irreversível!",
"optional": "Opcional",
"feedback_help": "(mas realmente nos ajuda a melhorar!)",
"reason_for_cancellation": "Motivo do cancelamento",
"please_specify": "Por favor, especifique",
"customer_service": "Atendimento ao Cliente",
"low_quality": "Baixa Qualidade",
"missing_features": "Recursos Faltando",
"switched_service": "Serviço Trocado",
"too_complex": "Muito Complexo",
"too_expensive": "Muito Caro",
"unused": "Não Utilizado",
"other": "Outro",
"more_information": "Mais informações (quanto mais detalhes, mais útil será)",
"feedback_placeholder": "por exemplo, eu precisava de um recurso que...",
"delete_your_account": "Excluir sua conta",
"change_password": "Alterar senha",
"password_length_error": "As senhas devem ter pelo menos 8 caracteres.",
"applying_changes": "Aplicando...",
"password_change_instructions": "Para alterar sua senha, preencha os seguintes campos. Sua senha deve ter pelo menos 8 caracteres.",
"old_password": "Senha Antiga",
"new_password": "Nova Senha",
"preference": "Preferência",
"select_theme": "Selecionar Tema",
"dark": "Escuro",
"light": "Claro",
"light_icon": "Claro",
"archive_settings": "Configurações de Arquivo",
"formats_to_archive": "Formatos para Arquivar/Preservar páginas da web:",
"screenshot": "Captura de Tela",
"search": "Busca",
"pdf": "PDF",
"archive_org_snapshot": "Instantâneo do Archive.org",
"link_settings": "Configurações de Link",
"prevent_duplicate_links": "Prevenir links duplicados",
"preview_unavailable": "Pré-Visualização Indisponível",
"clicking_on_links_should": "Clicar em Links deve:",
"open_original_content": "Abrir o conteúdo original",
"open_pdf_if_available": "Abrir PDF, se disponível",
"open_readable_if_available": "Abrir Legível, se disponível",
"open_screenshot_if_available": "Abrir Captura de Tela, se disponível",
"open_webpage_if_available": "Abrir cópia da página da Web, se disponível",
"tag_renamed": "Tag renomeada!",
"tag_deleted": "Tag excluída!",
"rename_tag": "Renomear Tag",
"delete_tag": "Excluir Tag",
"list_created_with_linkwarden": "Lista criada com Linkwarden",
"by_author": "Por {{author}}.",
"by_author_and_other": "Por {{author}} e {{count}} outro.",
"by_author_and_others": "Por {{author}} e {{count}} outros.",
"search_count_link": "Pesquisar {{count}} Link",
"search_count_links": "Pesquisar {{count}} Links",
"collection_is_empty": "Esta Coleção está vazia...",
"all_links": "Todos os Links",
"all_links_desc": "Links de todas as Coleções",
"you_have_not_added_any_links": "Você ainda não criou nenhum Link",
"collections_you_own": "Coleções que você possui",
"new_collection": "Nova Coleção",
"other_collections": "Outras Coleções",
"other_collections_desc": "Coleções compartilhadas das quais você é membro",
"showing_count_results": "Mostrando {{count}} resultados",
"showing_count_result": "Mostrando {{count}} resultado",
"edit_collection_info": "Editar Informações da Coleção",
"share_and_collaborate": "Compartilhar e Colaborar",
"view_team": "Ver Equipe",
"team": "Equipe",
"create_subcollection": "Criar Subcoleção",
"delete_collection": "Excluir Coleção",
"leave_collection": "Sair da Coleção",
"email_verified_signing_out": "E-mail verificado. Saindo...",
"invalid_token": "Token inválido.",
"sending_password_recovery_link": "Enviando link de recuperação de senha...",
"please_fill_all_fields": "Por favor, preencha todos os campos.",
"password_updated": "Senha Atualizada!",
"reset_password": "Redefinir Senha",
"enter_email_for_new_password": "Digite seu e-mail para que possamos enviar um link para criar uma nova senha.",
"update_password": "Atualizar Senha",
"password_successfully_updated": "Sua senha foi atualizada com sucesso.",
"user_already_member": "O usuário já existe.",
"you_are_already_collection_owner": "Você já é o proprietário da coleção.",
"date_newest_first": "Data (Mais Recente Primeiro)",
"date_oldest_first": "Data (Mais Antigo Primeiro)",
"default": "Padrão",
"name_az": "Nome (A-Z)",
"name_za": "Nome (Z-A)",
"description_az": "Descrição (A-Z)",
"description_za": "Descrição (Z-A)",
"all_rights_reserved": "© {{date}} Linkwarden. Todos os direitos reservados.",
"you_have_no_collections": "Você não tem Coleções...",
"you_have_no_tags": "Você não tem Tags...",
"cant_change_collection_you_dont_own": "Você não pode fazer alterações em uma coleção que você não possui.",
"change_icon": "Alterar Ícone",
"account": "Conta",
"billing": "Faturamento",
"linkwarden_version": "Linkwarden {{version}}",
"help": "Ajuda",
"github": "GitHub",
"twitter": "Twitter",
"mastodon": "Mastodon",
"link_preservation_in_queue": "A preservação do Link está atualmente na fila",
"check_back_later": "Por favor, verifique mais tarde para ver o resultado",
"there_are_more_formats": "Há mais formatos preservados na fila",
"thin": "Fino",
"settings": "Configurações",
"switch_to": "Mudar para {{theme}}",
"logout": "Sair",
"start_journey": "Comece sua jornada criando um novo Link!",
"create_new_link": "Criar Novo Link",
"new_link": "Novo Link",
"create_new": "Criar Novo...",
"pwa_install_prompt": "Instale o Linkwarden na sua tela inicial para um acesso mais rápido e uma experiência aprimorada. Saiba mais",
"full_content": "Conteúdo Completo",
"slower": "Mais Lento",
"new_version_announcement": "Veja as novidades em Linkwarden {{version}}!",
"creating": "Criando...",
"upload_file": "Enviar Arquivo",
"file": "Arquivo",
"file_types": "PDF, PNG, JPG (Até {{size}} MB)",
"fill": "Preencher",
"description": "Descrição",
"auto_generated": "Será gerado automaticamente se nada for fornecido.",
"example_link": "por exemplo, Link de Exemplo",
"hide": "Ocultar",
"more": "Mais",
"options": "Opções",
"description_placeholder": "Notas, pensamentos, etc.",
"deleting": "Excluindo...",
"token_revoked": "Token Revogado.",
"revoke_token": "Revogar Token",
"revoke_confirmation": "Tem certeza de que deseja revogar este Token de Acesso? Quaisquer aplicativos ou serviços que usem este token não poderão mais acessar o Linkwarden usando-o.",
"revoke": "Revogar",
"sending_request": "Enviando solicitação...",
"link_being_archived": "O link está sendo arquivado...",
"preserved_formats": "Formatos Preservados",
"available_formats": "Os seguintes formatos estão disponíveis para este link",
"readable": "Legível",
"preservation_in_queue": "A preservação do link está na fila",
"view_latest_snapshot": "Ver o último snapshot em archive.org",
"refresh_preserved_formats": "Atualizar Formatos Preservados",
"this_deletes_current_preservations": "Isso exclui as preservações atuais",
"create_new_user": "Criar Novo Usuário",
"placeholder_johnny": "Johnny",
"placeholder_email": "johnny@example.com",
"placeholder_john": "john",
"user_created": "Usuário Criado!",
"fill_all_fields_error": "Por favor, preencha todos os campos.",
"password_change_note": "Nota: Certifique-se de informar o usuário que ele precisa alterar sua senha.",
"create_user": "Criar Usuário",
"creating_token": "Criando Token...",
"token_created": "Token Criado!",
"access_token_created": "Token de Acesso Criado",
"token_creation_notice": "Seu novo token foi criado. Por favor, copie-o e guarde-o em um local seguro. Você não poderá vê-lo novamente.",
"copied_to_clipboard": "Copiado para a área de transferência!",
"copy_to_clipboard": "Copiar para a Área de Transferência",
"create_access_token": "Criar um Token de Acesso",
"expires_in": "Expira em",
"token_name_placeholder": "por exemplo, Para o atalho do iOS",
"create_token": "Criar Token de Acesso",
"7_days": "7 Dias",
"30_days": "30 Dias",
"60_days": "60 Dias",
"90_days": "90 Dias",
"no_expiration": "Sem Expiração",
"no_tags": "Sem tags.",
"creating_link": "Criando link...",
"link_created": "Link criado!",
"link_name_placeholder": "Será gerado automaticamente se deixado em branco.",
"link_url_placeholder": "por exemplo, http://example.com/",
"link_description_placeholder": "Notas, pensamentos, etc.",
"more_options": "Mais Opções",
"hide_options": "Ocultar Opções",
"icon": "Icone",
"image": "Imagem",
"create_link": "Criar Link",
"new_sub_collection": "Nova Subcoleção",
"for_collection": "Para {{name}}",
"create_new_collection": "Criar uma Nova Coleção",
"color": "Cor",
"columns": "Colunas",
"reset": "Redefinir",
"updating_collection": "Atualizando Coleção...",
"collection_name_placeholder": "por exemplo, Coleção de Exemplo",
"collection_description_placeholder": "O propósito desta Coleção...",
"create_collection_button": "Criar Coleção",
"password_change_warning": "Por favor, confirme sua senha antes de alterar seu endereço de e-mail.",
"stripe_update_note": " Atualizar este campo também alterará seu e-mail de cobrança no Stripe.",
"sso_will_be_removed_warning": "Se você alterar seu endereço de e-mail, todas as conexões SSO {{service}} existentes serão removidas.",
"old_email": "E-mail Antigo",
"new_email": "Novo E-mail",
"confirm": "Confirmar",
"edit_link": "Editar Link",
"updating": "Atualizando...",
"updated": "Atualizado!",
"placeholder_example_link": "por exemplo, Link de Exemplo",
"make_collection_public": "Tornar a Coleção Pública",
"make_collection_public_checkbox": "Tornar esta uma coleção pública",
"make_collection_public_desc": "Isso permitirá que qualquer pessoa visualize esta coleção e seus usuários.",
"sharable_link_guide": "Link Compartilhável (Clique para copiar)",
"copied": "Copiado!",
"members": "Membros",
"members_username_placeholder": "Nome de usuário (sem o '@')",
"owner": "Proprietário",
"admin": "Administrador",
"contributor": "Colaborador",
"viewer": "Visualizador",
"viewer_desc": "Acesso somente leitura",
"contributor_desc": "Pode visualizar e criar Links",
"admin_desc": "Acesso total a todos os Links",
"remove_member": "Remover Membro",
"placeholder_example_collection": "por exemplo, Coleção de Exemplo",
"placeholder_collection_purpose": "O propósito desta Coleção...",
"deleting_user": "Excluindo...",
"user_deleted": "Usuário Excluído.",
"delete_user": "Excluir Usuário",
"confirm_user_deletion": "Tem certeza de que deseja remover este usuário?",
"irreversible_action_warning": "Esta ação é irreversível!",
"delete_confirmation": "Excluir, eu sei o que estou fazendo",
"delete_link": "Excluir Link",
"deleted": "Excluído.",
"link_deletion_confirmation_message": "Tem certeza de que deseja excluir este Link?",
"warning": "Aviso",
"irreversible_warning": "Esta ação é irreversível!",
"shift_key_tip": "Mantenha a tecla Shift pressionada enquanto clica em 'Excluir' para ignorar esta confirmação no futuro.",
"show": "Exibir",
"deleting_collection": "Excluindo...",
"collection_deleted": "Coleção Excluída.",
"confirm_deletion_prompt": "Para confirmar, digite \"{{name}}\" na caixa abaixo:",
"type_name_placeholder": "Digite \"{{name}}\" Aqui.",
"deletion_warning": "Excluir esta coleção apagará permanentemente todo o seu conteúdo e ela se tornará inacessível para todos, incluindo membros com acesso anterior.",
"leave_prompt": "Clique no botão abaixo para sair da coleção atual.",
"leave": "Sair",
"edit_links": "Editar {{count}} Links",
"move_to_collection": "Mover para a Coleção",
"add_tags": "Adicionar Tags",
"remove_previous_tags": "Remover tags anteriores",
"delete_links": "Excluir {{count}} Links",
"links_deletion_confirmation_message": "Tem certeza de que deseja excluir {{count}} Links? ",
"warning_irreversible": "Aviso: Esta ação é irreversível!",
"shift_key_instruction": "Mantenha a tecla 'Shift' pressionada enquanto clica em 'Excluir' para ignorar esta confirmação no futuro.",
"link_selection_error": "Você não tem permissão para editar ou excluir este item.",
"no_description": "Nenhuma descrição fornecida.",
"no_description_provided": "Nenhuma descrição fornecida.",
"applying": "Aplicando...",
"unpin": "Desafixar",
"untitled": "Sem Título",
"pin_to_dashboard": "Fixar no Painel",
"show_link_details": "Mostrar Detalhes do Link",
"link_pinned": "Link Fixado!",
"link_unpinned": "Link Desafixado!",
"webpage": "Página da Web",
"server_administration": "Administração do Servidor",
"set_custom_icon": "Definir Ícone Customizado",
"all_collections": "Todas as Coleções",
"dashboard": "Painel",
"demo_title": "Apenas Demonstração",
"demo_desc": "Esta é apenas uma instância de demonstração do Linkwarden e os uploads estão desativados.",
"demo_desc_2": "Se você quiser experimentar a versão completa, você pode se inscrever para um teste gratuito em:",
"demo_button": "Entrar como usuário de demonstração",
"notes": "Notas"
}

View File

@@ -0,0 +1,377 @@
{
"user_administration": "Kullanıcı Yönetimi",
"search_users": "Kullanıcı Ara",
"no_users_found": "Kullanıcı bulunamadı.",
"no_user_found_in_search": "Verilen arama sorgusuyla kullanıcı bulunamadı.",
"username": "Kullanıcı Adı",
"email": "E-posta",
"subscribed": "Abone",
"created_at": "Oluşturulma Tarihi",
"not_available": "Mevcut Değil",
"check_your_email": "Lütfen E-postanızı Kontrol Edin",
"authenticating": "Kimlik doğrulanıyor...",
"verification_email_sent": "Doğrulama e-postası gönderildi.",
"verification_email_sent_desc": "Giriş bağlantısı e-posta adresinize gönderildi. E-postayı görmüyorsanız, spam klasörünüzü kontrol edin.",
"resend_email": "E-postayı Tekrar Gönder",
"invalid_credentials": "Geçersiz kimlik bilgileri.",
"fill_all_fields": "Lütfen tüm alanları doldurun.",
"enter_credentials": "Kimlik bilgilerinizi girin",
"username_or_email": "Kullanıcı Adı veya E-posta",
"password": "Şifre",
"confirm_password": "Şifreyi Onayla",
"forgot_password": "Şifrenizi mi unuttunuz?",
"login": "Giriş Yap",
"or_continue_with": "Veya devam edin",
"new_here": "Yeni misiniz?",
"sign_up": "Kayıt Ol",
"sign_in_to_your_account": "Hesabınıza giriş yapın",
"dashboard_desc": "Verilerinizin kısa bir özeti",
"link": "Bağlantı",
"links": "Bağlantılar",
"collection": "Koleksiyon",
"collections": "Koleksiyonlar",
"tag": "Etiket",
"tags": "Etiketler",
"recent": "Son",
"recent_links_desc": "Son Eklenen Bağlantılar",
"view_all": "Tümünü Görüntüle",
"view_added_links_here": "Son Eklediğiniz Bağlantıları Buradan Görüntüleyin!",
"view_added_links_here_desc": "Bu bölüm, erişiminiz olan tüm Koleksiyonlardaki en son eklediğiniz Bağlantıları görüntüler.",
"add_link": "Yeni Bağlantı Ekle",
"import_links": "Bağlantıları İçe Aktar",
"from_linkwarden": "Linkwarden'dan",
"from_html": "Yer İmleri HTML dosyasından",
"from_wallabag": "Wallabag'dan (JSON dosyası)",
"pinned": "Sabitlenmiş",
"pinned_links_desc": "Sabitlenmiş Bağlantılarınız",
"pin_favorite_links_here": "Favori Bağlantılarınızı Buraya Sabitleyin!",
"pin_favorite_links_here_desc": "Favori Bağlantılarınızı, her Bağlantının üzerindeki üç noktaya tıklayarak ve Pano'ya Sabitle seçeneğini seçerek sabitleyebilirsiniz.",
"sending_password_link": "Şifre sıfırlama bağlantısı gönderiliyor...",
"password_email_prompt": "Yeni bir şifre oluşturabilmeniz için e-posta adresinizi girin, size bir bağlantı göndereceğiz.",
"send_reset_link": "Sıfırlama Bağlantısını Gönder",
"reset_email_sent_desc": "Şifrenizi sıfırlamak için e-postanızı kontrol edin. Birkaç dakika içinde gelmezse, spam klasörünüzü kontrol edin.",
"back_to_login": "Girişe Geri Dön",
"email_sent": "E-posta Gönderildi!",
"passwords_mismatch": "Şifreler uyuşmuyor.",
"password_too_short": "Şifreler en az 8 karakter olmalıdır.",
"creating_account": "Hesap Oluşturuluyor...",
"account_created": "Hesap Oluşturuldu!",
"trial_offer_desc": "{{count}} gün Ücretsiz Premium Hizmetin kilidini açın!",
"register_desc": "Yeni bir hesap oluştur",
"registration_disabled_desc": "Bu sunucu için kayıt devre dışı bırakıldı, herhangi bir sorun yaşarsanız lütfen yönetici ile iletişime geçin.",
"enter_details": "Bilgilerinizi girin",
"display_name": "Görünen Ad",
"sign_up_agreement": "Kaydolarak, <0>Hizmet Şartlarımızı</0> ve <1>Gizlilik Politikamızı</1> kabul etmiş olursunuz.",
"need_help": "Yardıma mı ihtiyacınız var?",
"get_in_touch": "İletişime geçin",
"already_registered": "Zaten hesabınız var mı?",
"deleting_selections": "Seçimler Siliniyor...",
"links_deleted": "{{count}} Bağlantı silindi.",
"link_deleted": "1 Bağlantı silindi.",
"links_selected": "{{count}} Bağlantı seçildi",
"link_selected": "1 Bağlantı seçildi",
"nothing_selected": "Hiçbir şey seçilmedi",
"edit": "Düzenle",
"delete": "Sil",
"nothing_found": "Hiçbir şey bulunamadı.",
"redirecting_to_stripe": "Stripe'a yönlendiriliyor...",
"subscribe_title": "Linkwarden'a Abone Olun!",
"subscribe_desc": "Stripe'a yönlendirileceksiniz, herhangi bir sorun yaşarsanız <0>support@linkwarden.app</0> adresinden bizimle iletişime geçebilirsiniz.",
"monthly": "Aylık",
"yearly": "Yıllık",
"discount_percent": "{{percent}}% İndirim",
"billed_monthly": "Aylık Faturalandırılır",
"billed_yearly": "Yıllık Faturalandırılır",
"total": "Toplam",
"total_annual_desc": "{{count}} gün ücretsiz deneme süresi, ardından yıllık {{annualPrice}} $",
"total_monthly_desc": "{{count}} gün ücretsiz deneme süresi, ardından aylık {{monthlyPrice}} $",
"plus_tax": "+ Uygun ise KDV",
"complete_subscription": "Aboneliği Tamamla",
"sign_out": ıkış Yap",
"access_tokens": "Erişim Tokenleri",
"access_tokens_description": "Erişim Tokenleri, Kullanıcı Adı ve Şifrenizi vermeden Linkwarden'a diğer uygulamalar ve hizmetler aracılığıyla erişmek için kullanılabilir.",
"new_token": "Yeni Erişim Tokeni",
"name": "İsim",
"created_success": "Oluşturuldu!",
"created": "Oluşturuldu",
"expires": "Son Kullanma Tarihi",
"accountSettings": "Hesap Ayarları",
"language": "Dil",
"profile_photo": "Profil Fotoğrafı",
"upload_new_photo": "Yeni bir fotoğraf yükle...",
"remove_photo": "Fotoğrafı Kaldır",
"make_profile_private": "Profili gizli yap",
"profile_privacy_info": "Bu, kimlerin sizi bulup yeni Koleksiyonlara ekleyebileceğini sınırlayacaktır.",
"whitelisted_users": "Beyaz Listeye Alınmış Kullanıcılar",
"whitelisted_users_info": "Profilinize görünürlük sağlamak istediğiniz kullanıcı adlarını belirtin. Virgülle ayrılmış olarak.",
"whitelisted_users_placeholder": "Profiliniz şu anda herkesten gizlenmiştir...",
"save_changes": "Değişiklikleri Kaydet",
"import_export": "İçe/Dışa Aktar",
"import_data": "Verilerinizi diğer platformlardan içe aktarın.",
"download_data": "Verilerinizi hemen indirin.",
"export_data": "Verileri Dışa Aktar",
"delete_account": "Hesabı Sil",
"delete_account_warning": "Bu işlem, SAHİP OLDUĞUNUZ tüm Bağlantılar, Koleksiyonlar, Etiketler ve arşivlenmiş verileri KALICI OLARAK silecektir.",
"cancel_subscription_notice": "Ayrıca aboneliğinizi de iptal eder.",
"account_deletion_page": "Hesap silme sayfası",
"applying_settings": "Ayarlar Uygulanıyor...",
"settings_applied": "Ayarlar Uygulandı!",
"email_change_request": "E-posta değiştirme isteği gönderildi. Lütfen yeni e-posta adresinizi doğrulayın.",
"image_upload_size_error": "Lütfen 1MB'tan küçük bir PNG veya JPEG dosyası seçin.",
"image_upload_format_error": "Geçersiz dosya formatı.",
"importing_bookmarks": "Yer imleri içe aktarılıyor...",
"import_success": "Yer imleri içe aktarıldı! Sayfa yeniden yükleniyor...",
"more_coming_soon": "Daha fazlası yakında!",
"billing_settings": "Faturalandırma Ayarları",
"manage_subscription_intro": "Aboneliğinizi yönetmek/iptal etmek için, şu adrese gidin:",
"billing_portal": "Fatura Portalı",
"help_contact_intro": "Hala yardıma ihtiyacınız varsa veya herhangi bir sorun yaşadıysanız, bizimle iletişime geçmekten çekinmeyin:",
"fill_required_fields": "Lütfen gerekli alanları doldurun.",
"deleting_message": "Her şey siliniyor, lütfen bekleyin...",
"delete_warning": "Bu işlem, SAHİP OLDUĞUNUZ tüm Bağlantılar, Koleksiyonlar, Etiketler ve arşivlenmiş verileri KALICI OLARAK silecektir. Bu işlem ayrıca çıkış yapmanıza neden olacaktır. Bu işlem geri alınamaz!",
"optional": "İsteğe bağlı",
"feedback_help": "(ama bu gerçekten gelişmemize yardımcı olur!)",
"reason_for_cancellation": "İptal sebebi",
"please_specify": "Lütfen belirtin",
"customer_service": "Müşteri Hizmetleri",
"low_quality": "Düşük Kalite",
"missing_features": "Eksik Özellikler",
"switched_service": "Hizmeti Değiştirdim",
"too_complex": "Çok Karmaşık",
"too_expensive": "Çok Pahalı",
"unused": "Kullanılmıyor",
"other": "Diğer",
"more_information": "Daha fazla bilgi (ne kadar ayrıntı, o kadar faydalı olur)",
"feedback_placeholder": "ör. Bir özellik gerekiyordu...",
"delete_your_account": "Hesabınızı Silin",
"change_password": "Şifreyi Değiştir",
"password_length_error": "Şifreler en az 8 karakter olmalıdır.",
"applying_changes": "Değişiklikler Uygulanıyor...",
"password_change_instructions": "Şifrenizi değiştirmek için lütfen aşağıdaki bilgileri doldurun. Şifreniz en az 8 karakter olmalıdır.",
"old_password": "Eski Şifre",
"new_password": "Yeni Şifre",
"preference": "Tercih",
"select_theme": "Tema Seçin",
"dark": "Koyu",
"light": "Açık",
"archive_settings": "Arşiv Ayarları",
"formats_to_archive": "Web sayfalarını Arşivlemek/Korumak için formatlar:",
"screenshot": "Ekran Görüntüsü",
"pdf": "PDF",
"archive_org_snapshot": "Archive.org Anlık Görüntü",
"link_settings": "Bağlantı Ayarları",
"prevent_duplicate_links": "Yinelenen bağlantıları önle",
"clicking_on_links_should": "Bağlantılara tıklandığında:",
"open_original_content": "Orijinal içeriği aç",
"open_pdf_if_available": "Mevcutsa PDF aç",
"open_readable_if_available": "Mevcutsa Okunabilir aç",
"open_screenshot_if_available": "Mevcutsa Ekran Görüntüsü aç",
"open_webpage_if_available": "Mevcutsa Web sayfası kopyasını aç",
"tag_renamed": "Etiket yeniden adlandırıldı!",
"tag_deleted": "Etiket silindi!",
"rename_tag": "Etiketi Yeniden Adlandır",
"delete_tag": "Etiketi Sil",
"list_created_with_linkwarden": "Bu liste Linkwarden ile oluşturuldu",
"by_author": "{{author}} tarafından.",
"by_author_and_other": "{{author}} ve {{count}} diğer tarafından.",
"by_author_and_others": "{{author}} ve {{count}} diğerleri tarafından.",
"search_count_link": "{{count}} Bağlantı Ara",
"search_count_links": "{{count}} Bağlantı Ara",
"collection_is_empty": "Bu Koleksiyon boş...",
"all_links": "Tüm Bağlantılar",
"all_links_desc": "Her Koleksiyondan Bağlantılar",
"you_have_not_added_any_links": "Henüz Bağlantı Oluşturmadınız",
"collections_you_own": "Sahip Olduğunuz Koleksiyonlar",
"new_collection": "Yeni Koleksiyon",
"other_collections": "Diğer Koleksiyonlar",
"other_collections_desc": "Üyesi olduğunuz paylaşılan koleksiyonlar",
"showing_count_results": "{{count}} sonuç gösteriliyor",
"showing_count_result": "{{count}} sonuç gösteriliyor",
"edit_collection_info": "Koleksiyon Bilgilerini Düzenle",
"share_and_collaborate": "Paylaş ve İş Birliği Yap",
"view_team": "Takımı Görüntüle",
"team": "Takım",
"create_subcollection": "Alt Koleksiyon Oluştur",
"delete_collection": "Koleksiyonu Sil",
"leave_collection": "Koleksiyondan Ayrıl",
"email_verified_signing_out": "E-posta doğrulandı. Çıkış yapılıyor...",
"invalid_token": "Geçersiz token.",
"sending_password_recovery_link": "Şifre sıfırlama bağlantısı gönderiliyor...",
"please_fill_all_fields": "Lütfen tüm alanları doldurun.",
"password_updated": "Şifre Güncellendi!",
"reset_password": "Şifreyi Sıfırla",
"enter_email_for_new_password": "Yeni bir şifre oluşturabilmeniz için e-posta adresinizi girin, size bir bağlantı göndereceğiz.",
"update_password": "Şifreyi Güncelle",
"password_successfully_updated": "Şifreniz başarıyla güncellendi.",
"user_already_member": "Kullanıcı zaten mevcut.",
"you_are_already_collection_owner": "Zaten koleksiyon sahibisiniz.",
"date_newest_first": "Tarih (En Yeniler İlk)",
"date_oldest_first": "Tarih (En Eskiler İlk)",
"name_az": "Ad (A-Z)",
"name_za": "Ad (Z-A)",
"description_az": "Açıklama (A-Z)",
"description_za": "Açıklama (Z-A)",
"all_rights_reserved": "© {{date}} <0>Linkwarden</0>. Tüm hakları saklıdır.",
"you_have_no_collections": "Koleksiyonunuz yok...",
"you_have_no_tags": "Etiketiniz yok...",
"cant_change_collection_you_dont_own": "Sahibi olmadığınız bir koleksiyonu değiştiremezsiniz.",
"account": "Hesap",
"billing": "Faturalandırma",
"linkwarden_version": "Linkwarden {{version}}",
"help": "Yardım",
"github": "GitHub",
"twitter": "Twitter",
"mastodon": "Mastodon",
"link_preservation_in_queue": "Bağlantı koruma şu anda sırada",
"check_back_later": "Sonucu görmek için daha sonra tekrar kontrol edin",
"there_are_more_formats": "Sırada daha fazla korunmuş format var",
"settings": "Ayarlar",
"switch_to": "{{theme}}'ya geç",
"logout": ıkış Yap",
"start_journey": "Yeni bir Bağlantı oluşturarak yolculuğunuza başlayın!",
"create_new_link": "Yeni Bağlantı Oluştur",
"new_link": "Yeni Bağlantı",
"create_new": "Yeni Oluştur...",
"pwa_install_prompt": "Daha hızlı erişim ve geliştirilmiş bir deneyim için Linkwarden'ı ana ekranınıza yükleyin. <0>Daha fazla öğren</0>",
"full_content": "Tam İçerik",
"slower": "Daha Yavaş",
"new_version_announcement": "<0>Linkwarden {{version}}'da</0> nelerin yeni olduğunu görün!",
"creating": "Oluşturuluyor...",
"upload_file": "Dosya Yükle",
"file": "Dosya",
"file_types": "PDF, PNG, JPG (En fazla {{size}} MB)",
"description": "Açıklama",
"auto_generated": "Bir şey sağlanmazsa otomatik olarak oluşturulacaktır.",
"example_link": "ör. Örnek Bağlantı",
"hide": "Gizle",
"more": "Daha Fazla",
"options": "Seçenekler",
"description_placeholder": "Notlar, düşünceler, vb.",
"deleting": "Siliniyor...",
"token_revoked": "Token İptal Edildi.",
"revoke_token": "Token İptal Et",
"revoke_confirmation": "Bu Erişim Tokenini iptal etmek istediğinizden emin misiniz? Bu tokeni kullanan uygulamalar veya hizmetler artık Linkwarden'a erişemeyecektir.",
"revoke": "İptal Et",
"sending_request": "İstek Gönderiliyor...",
"link_being_archived": "Bağlantı arşivleniyor...",
"preserved_formats": "Korunan Formatlar",
"available_formats": "Bu bağlantı için kullanılabilir formatlar",
"readable": "Okunabilir",
"preservation_in_queue": "Bağlantı koruma sırada",
"view_latest_snapshot": "archive.org'da son anlık görüntüyü görüntüle",
"refresh_preserved_formats": "Korunan Formatları Yenile",
"this_deletes_current_preservations": "Bu mevcut korumaları siler",
"create_new_user": "Yeni Kullanıcı Oluştur",
"placeholder_johnny": "Johnny",
"placeholder_email": "johnny@example.com",
"placeholder_john": "john",
"user_created": "Kullanıcı Oluşturuldu!",
"fill_all_fields_error": "Lütfen tüm alanları doldurun.",
"password_change_note": "<0>Not:</0> Lütfen kullanıcıya şifresini değiştirmesi gerektiğini bildirdiğinizden emin olun.",
"create_user": "Kullanıcı Oluştur",
"creating_token": "Token Oluşturuluyor...",
"token_created": "Token Oluşturuldu!",
"access_token_created": "Erişim Tokeni Oluşturuldu",
"token_creation_notice": "Yeni tokeniniz oluşturuldu. Lütfen bunu kopyalayın ve güvenli bir yerde saklayın. Tekrar göremezsiniz.",
"copied_to_clipboard": "Panoya kopyalandı!",
"copy_to_clipboard": "Panoya Kopyala",
"create_access_token": "Erişim Tokeni Oluştur",
"expires_in": "Şunun Süresi Doluyor",
"token_name_placeholder": "ör. iOS kısayolu için",
"create_token": "Erişim Tokeni Oluştur",
"7_days": "7 Gün",
"30_days": "30 Gün",
"60_days": "60 Gün",
"90_days": "90 Gün",
"no_expiration": "Son Kullanma Tarihi Yok",
"creating_link": "Bağlantı Oluşturuluyor...",
"link_created": "Bağlantı Oluşturuldu!",
"link_name_placeholder": "Boş bırakılırsa otomatik olarak oluşturulacaktır.",
"link_url_placeholder": "ör. http://example.com/",
"link_description_placeholder": "Notlar, düşünceler, vb.",
"more_options": "Daha Fazla Seçenek",
"hide_options": "Seçenekleri Gizle",
"create_link": "Bağlantı Oluştur",
"new_sub_collection": "Yeni Alt Koleksiyon",
"for_collection": "{{name}} için",
"create_new_collection": "Yeni Koleksiyon Oluştur",
"color": "Renk",
"reset": "Sıfırla",
"updating_collection": "Koleksiyon Güncelleniyor...",
"collection_name_placeholder": "ör. Örnek Koleksiyon",
"collection_description_placeholder": "Bu Koleksiyonun amacı...",
"create_collection_button": "Koleksiyon Oluştur",
"password_change_warning": "E-posta adresinizi değiştirmeden önce şifrenizi doğrulayın.",
"stripe_update_note": "Bu alanı güncellemek, Stripe üzerindeki fatura e-posta adresinizi de değiştirir.",
"sso_will_be_removed_warning": "E-posta adresinizi değiştirirseniz, mevcut {{service}} SSO bağlantıları kaldırılacaktır.",
"old_email": "Eski E-posta",
"new_email": "Yeni E-posta",
"confirm": "Onayla",
"edit_link": "Bağlantıyı Düzenle",
"updating": "Güncelleniyor...",
"updated": "Güncellendi!",
"placeholder_example_link": "ör. Örnek Bağlantı",
"make_collection_public": "Koleksiyonu Halka Aç",
"make_collection_public_checkbox": "Bu bir kamu koleksiyonu olsun",
"make_collection_public_desc": "Bu, herkesin bu koleksiyonu ve kullanıcılarını görüntülemesini sağlar.",
"sharable_link_guide": "Paylaşılabilir Bağlantı (Kopyalamak için tıklayın)",
"copied": "Kopyalandı!",
"members": "Üyeler",
"members_username_placeholder": "Kullanıcı adı (\"@\" olmadan)",
"owner": "Sahip",
"admin": "Yönetici",
"contributor": "Katkıda Bulunan",
"viewer": "Görüntüleyici",
"viewer_desc": "Salt okunur erişim",
"contributor_desc": "Bağlantıları görüntüleyebilir ve oluşturabilir",
"admin_desc": "Tüm Bağlantılara tam erişim",
"remove_member": "Üyeyi Kaldır",
"placeholder_example_collection": "ör. Örnek Koleksiyon",
"placeholder_collection_purpose": "Bu Koleksiyonun amacı...",
"deleting_user": "Siliniyor...",
"user_deleted": "Kullanıcı Silindi.",
"delete_user": "Kullanıcıyı Sil",
"confirm_user_deletion": "Bu kullanıcıyı silmek istediğinizden emin misiniz?",
"irreversible_action_warning": "Bu işlem geri alınamaz!",
"delete_confirmation": "Sil, ne yaptığımı biliyorum",
"delete_link": "Bağlantıyı Sil",
"deleted": "Silindi.",
"link_deletion_confirmation_message": "Bu Bağlantıyı silmek istediğinizden emin misiniz?",
"warning": "Uyarı",
"irreversible_warning": "Bu işlem geri alınamaz!",
"shift_key_tip": "Gelecekte bu onayı atlamak için 'Sil' tuşuna tıklarken Shift tuşunu basılı tutun.",
"deleting_collection": "Siliniyor...",
"collection_deleted": "Koleksiyon Silindi.",
"confirm_deletion_prompt": "Onaylamak için aşağıdaki kutuya \"{{name}}\" yazın:",
"type_name_placeholder": "\"{{name}}\" Buraya Yazın.",
"deletion_warning": "Bu koleksiyonu silmek, tüm içeriğini kalıcı olarak silecek ve önceki erişime sahip üyeler dahil olmak üzere herkese erişilemez hale getirecektir.",
"leave_prompt": "Mevcut koleksiyondan ayrılmak için aşağıdaki düğmeye tıklayın.",
"leave": "Ayrıl",
"edit_links": "{{count}} Bağlantıyı Düzenle",
"move_to_collection": "Koleksiyona Taşı",
"add_tags": "Etiket Ekle",
"remove_previous_tags": "Önceki etiketleri kaldır",
"delete_links": "{{count}} Bağlantıyı Sil",
"links_deletion_confirmation_message": "{{count}} Bağlantıyı silmek istediğinizden emin misiniz?",
"warning_irreversible": "Uyarı: Bu işlem geri alınamaz!",
"shift_key_instruction": "Gelecekte bu onayı atlamak için 'Sil' tuşuna tıklarken Shift tuşunu basılı tutun.",
"link_selection_error": "Bu öğeyi düzenleme veya silme izniniz yok.",
"no_description": "Açıklama verilmedi.",
"applying": "Uygulanıyor...",
"unpin": "Sabitleneni Kaldır",
"pin_to_dashboard": "Panoya Sabitle",
"show_link_details": "Bağlantı Ayrıntılarını Göster",
"hide_link_details": "Bağlantı Ayrıntılarını Gizle",
"link_pinned": "Bağlantı Sabitlendi!",
"link_unpinned": "Bağlantı Sabitlendi!",
"webpage": "Web Sayfası",
"server_administration": "Sunucu Yönetimi",
"all_collections": "Tüm Koleksiyonlar",
"dashboard": "Kontrol Paneli",
"demo_title": "Sadece Demo",
"demo_desc": "Bu sadece bir Linkwarden demo örneğidir ve yüklemeler devre dışı bırakılmıştır.",
"demo_desc_2": "Tam sürümü denemek istiyorsanız, ücretsiz deneme için kaydolabilirsiniz:",
"demo_button": "Demo kullanıcı olarak giriş yap"
}

View File

@@ -0,0 +1,397 @@
{
"user_administration": "Адміністрування користувачів",
"search_users": "Пошук користувачів",
"no_users_found": "Користувачів не знайдено.",
"no_user_found_in_search": "За вказаним пошуковим запитом не знайдено користувачів.",
"username": "Ім'я користувача",
"email": "Електронна скринька",
"subscribed": "Підписаний",
"created_at": "Створено",
"not_available": "недоступний",
"check_your_email": "Будь ласка, перевірте свою електронну скриньку",
"authenticating": "Автентифікація...",
"verification_email_sent": "Надіслано електронний лист для підтвердження.",
"verification_email_sent_desc": "Посилання для входу надіслано на вашу адресу електронної скриньки. Якщо ви не бачите листа, перевірте папку зі спамом.",
"resend_email": "Повторно надіслати електронний лист",
"invalid_credentials": "Недійсні облікові дані.",
"fill_all_fields": "Будь ласка, заповніть усі поля.",
"enter_credentials": "Введіть свої облікові дані",
"username_or_email": "Ім'я користувача чи електронну пошту",
"password": "Пароль",
"confirm_password": "Підтвердьте пароль",
"forgot_password": "Забули пароль?",
"login": "Логін",
"or_continue_with": "Або продовжити",
"new_here": "Ви тут новий?",
"sign_up": "Зареєструватися",
"sign_in_to_your_account": "Увійдіть у свій обліковий запис",
"dashboard_desc": "Короткий огляд ваших даних",
"link": "Посилання",
"links": "Посилання",
"collection": "Колекція",
"collections": "Колекції",
"tag": "Мітка",
"tags": "Мітки",
"recent": "Недавні",
"recent_links_desc": "Нещодавно додані посилання",
"view_all": "Переглянути всі",
"view_added_links_here": "Перегляньте свої нещодавно додані посилання тут!",
"view_added_links_here_desc": "У цьому розділі відображатимуться ваші останні додані посилання в усіх колекціях, до яких ви маєте доступ.",
"add_link": "Додати нове посилання",
"import_links": "Імпорт посилань",
"from_linkwarden": "Від Linkwarden",
"from_html": "З HTML-файлу закладок",
"from_wallabag": "Від Wallabag (файл JSON)",
"pinned": "Закріплено",
"pinned_links_desc": "Ваші закріплені посилання",
"pin_favorite_links_here": "Закріпіть тут свої улюблені посилання!",
"pin_favorite_links_here_desc": "Ви можете закріпити свої улюблені посилання, натиснувши три крапки на кожному посиланні та натиснувши «Закріпити на інформаційній панелі».",
"sending_password_link": "Надсилання посилання для відновлення пароля...",
"password_email_prompt": "Введіть свою електронну адресу, щоб ми могли надіслати вам посилання для створення нового пароля.",
"send_reset_link": "Надіслати посилання для скидання (пароля)",
"reset_email_sent_desc": "Перевірте свою електронну пошту на наявність посилання для скидання пароля. Якщо він не з’явиться протягом кількох хвилин, перевірте папку зі спамом.",
"back_to_login": "Назад до входу",
"email_sent": "Електронна пошта надіслана!",
"passwords_mismatch": "Паролі не збігаються.",
"password_too_short": "Паролі мають бути не менше 8 символів.",
"creating_account": "Створення облікового запису...",
"account_created": "Обліковий запис створено!",
"trial_offer_desc": "Розблокуйте {{count}} днів преміумсервісу безплатно!",
"register_desc": "Створіть новий обліковий запис",
"registration_disabled_desc": "Для цього випадку реєстрацію вимкнено, у разі будь-яких проблем зв’яжіться з адміністратором.",
"enter_details": "Введіть свої дані",
"display_name": "Відображуване ім'я",
"sign_up_agreement": "Реєструючись, ви приймаєте наші <0>Загальні положення та умови</0> та <1>Політику конфіденційності</1>.",
"need_help": "Потрібна допомога?",
"get_in_touch": "Зв'яжіться",
"already_registered": "Вже маєте акаунт?",
"deleting_selections": "Видалення вибраних...",
"links_deleted": "{{count}} посилань видалено.",
"link_deleted": "1 посилання видалено.",
"links_selected": "Вибрано {{count}} посилань",
"link_selected": "Вибрано 1 посилання",
"nothing_selected": "Нічого не вибрано",
"edit": "Редагувати",
"delete": "Видалити",
"nothing_found": "Нічого не знайдено.",
"redirecting_to_stripe": "Переспрямування на сервіс Stripe...",
"subscribe_title": "Підпишіться на Linkwarden!",
"subscribe_desc": "Ви будете перенаправлені на сторінку сервісу Stripe. Якщо виникнуть проблеми, зв’яжіться з нами за адресою <0>support@linkwarden.app</0>.",
"monthly": "Щомісяця",
"yearly": "Щороку",
"discount_percent": "Знижка {{percent}}%",
"billed_monthly": "Рахунок виставляється щомісяця",
"billed_yearly": "Рахунок виставляється щорічно",
"total": "Всього",
"total_annual_desc": "{{count}}-денна безкоштовна пробна версія, а потім {{annualPrice}}$ щорічно",
"total_monthly_desc": "{{count}}-денна безкоштовна пробна версія, потім {{monthly Price}}$ на місяць",
"plus_tax": "+ ПДВ, якщо є",
"complete_subscription": "Повна підписка",
"sign_out": "Вийти",
"access_tokens": "Жетони доступу",
"access_tokens_description": "Жетони доступу можна використовувати для доступу до Linkwarden з інших програм і служб, не повідомляючи свого імені користувача та пароля.",
"new_token": "Новий жетон доступу",
"name": "Ім'я",
"created_success": "Створено!",
"created": "Створено",
"expires": "Термін дії закінчується",
"accountSettings": "Налаштування облікового запису",
"language": "Мова",
"profile_photo": "Фото профілю",
"upload_new_photo": "Завантажте нове фото...",
"remove_photo": "Видалити фото",
"make_profile_private": "Зробити профіль приватним",
"profile_privacy_info": "Це обмежить, хтось зможе знаходити вас і додавати до нових колекцій.",
"whitelisted_users": "Користувачі з білого списку",
"whitelisted_users_info": "Будь ласка, вкажіть ім’я користувача, якому ви хочете надати доступ до свого профілю. Розділяється комою.",
"whitelisted_users_placeholder": "Ваш профіль зараз прихований від усіх...",
"save_changes": "Зберегти зміни",
"import_export": "Імпорт та експорт",
"import_data": "Імпортуйте дані з інших платформ.",
"download_data": "Миттєво завантажуйте свої дані.",
"export_data": "Експорт даних",
"delete_account": "Видалити акаунт",
"delete_account_warning": "Це назавжди видалить УСІ посилання, колекції, мітки та архівні дані, якими ви володієте.",
"cancel_subscription_notice": "Це також скасує вашу підписку.",
"account_deletion_page": "Сторінка видалення облікового запису",
"applying_settings": "Застосування налаштувань...",
"settings_applied": "Налаштування застосовано!",
"email_change_request": "Запит на зміну електронної скриньки надіслано. Підтвердьте нову електронну скриньку.",
"image_upload_no_file_error": "Файл не вибрано. Будь ласка, виберіть зображення для завантаження.",
"image_upload_size_error": "Виберіть файл PNG або JPEG розміром менше ніж 1 МБ.",
"image_upload_format_error": "Невірний формат файлу.",
"importing_bookmarks": "Імпорт закладок...",
"import_success": "Імпортовано закладки! Перезавантаження сторінки...",
"more_coming_soon": "Більше смаколиків незабаром!",
"billing_settings": "Налаштування платежів",
"manage_subscription_intro": "Щоб керувати або скасувати підписку, відвідайте",
"billing_portal": "Платіжний портал",
"help_contact_intro": "Якщо вам все ще потрібна допомога або виникли проблеми, зв’яжіться з нами за адресою:",
"fill_required_fields": "Будь ласка, заповніть необхідні поля.",
"deleting_message": "Видалення всього, зачекайте...",
"delete_warning": "Це назавжди видалить усі посилання, колекції, мітки та архівні дані, якими ви володієте. Це також призведе до виходу з системи. Ця дія незворотна!",
"optional": "Необов'язково",
"feedback_help": "(але це дійсно допомагає нам покращуватися!)",
"reason_for_cancellation": "Причина скасування",
"please_specify": "Будь ласка, уточніть",
"customer_service": "Відділ обслуговування клієнтів",
"low_quality": "Низька якість",
"missing_features": "Відсутні функції",
"switched_service": "Зміна сервіса",
"too_complex": "Занадто складно",
"too_expensive": "Занадто дорого",
"unused": "Невикористаний",
"other": "інше",
"more_information": "Більше інформації (чим більше деталей, тим корисніше буде)",
"feedback_placeholder": "напр. Мені потрібна була функція, яка...",
"delete_your_account": "Видалити свій акаунт",
"change_password": "Змінити пароль",
"password_length_error": "Паролі мають бути не менше 8 символів.",
"applying_changes": "Застосування...",
"password_change_instructions": "Щоб змінити свій пароль, заповніть наступні поля. Ваш пароль має бути не менше 8 символів.",
"old_password": "Старий пароль",
"new_password": "Новий пароль",
"preference": "Перевага",
"select_theme": "Виберіть тему",
"dark": "темну",
"light": "світлу",
"archive_settings": "Налаштування архіву",
"formats_to_archive": "Формати для архівування/збереження вебсторінок:",
"screenshot": "Скриншот",
"pdf": "PDF",
"archive_org_snapshot": "Знімок Archive.org",
"link_settings": "Налаштування посилання",
"prevent_duplicate_links": "Запобігати повторюваним посиланням",
"clicking_on_links_should": "Натискання на посилання веде:",
"open_original_content": "Відкрити оригінальний вміст",
"open_pdf_if_available": "Відкрити PDF, якщо доступний",
"open_readable_if_available": "Відкрийте Readable, якщо доступно",
"open_screenshot_if_available": "Відкрийте скриншот, якщо доступний",
"open_webpage_if_available": "Відкрийте копію вебсторінки, якщо вона доступна",
"tag_renamed": "Мітка перейменована!",
"tag_deleted": "Мітка видалена!",
"rename_tag": "Перейменувати мітку",
"delete_tag": "Видалити мітку",
"list_created_with_linkwarden": "Список створено за допомогою Linkwarden",
"by_author": "Автор: {{author}}.",
"by_author_and_other": "Автор: {{author}} та ще {{count}} іншій.",
"by_author_and_others": "Автор: {{author}} та ще {{count}} іншіх.",
"search_count_link": "Пошук {{count}} посилання",
"search_count_links": "Пошук {{count}} посилань",
"collection_is_empty": "Ця колекція порожня...",
"all_links": "Усі посилання",
"all_links_desc": "Посилання з кожної колекції",
"you_have_not_added_any_links": "Ви ще не створили жодного посилання",
"collections_you_own": "Колекції, якими ви володієте",
"new_collection": "Нова колекція",
"other_collections": "Інші колекції",
"other_collections_desc": "Спільні колекції, учасником яких ви є",
"showing_count_results": "Показано {{count}} результатів",
"showing_count_result": "Показано {{count}} результата",
"edit_collection_info": "Редагувати інформацію про колекцію",
"share_and_collaborate": "Діліться та співпрацюйте",
"view_team": "Переглянути команду",
"team": "команда",
"create_subcollection": "Створення підколекції",
"delete_collection": "Видалити колекцію",
"leave_collection": "Залишити колекцію",
"email_verified_signing_out": "Електронна скринька підтверджена. Вихід...",
"invalid_token": "Недійсний жетон.",
"sending_password_recovery_link": "Надсилання посилання для відновлення пароля...",
"please_fill_all_fields": "Будь ласка, заповніть усі поля.",
"password_updated": "Пароль оновлено!",
"reset_password": "Скинути пароль",
"enter_email_for_new_password": "Введіть свою адресу електронної скриньки, щоб ми могли надіслати вам посилання для створення нового пароля.",
"update_password": "Оновити пароль",
"password_successfully_updated": "Ваш пароль успішно оновлено.",
"user_already_member": "Користувач уже існує.",
"you_are_already_collection_owner": "Ви вже є власником колекції.",
"date_newest_first": "Дата (спочатку найновіші)",
"date_oldest_first": "Дата (спочатку найдавніші)",
"name_az": "Ім'я, зростання за алфавітом (А-Я)",
"name_za": "Ім'я, спадання за алфавітом (Я-А)",
"description_az": "Опис, зростання за алфавітом (A-Z)",
"description_za": "Опис, спадання за алфавітом (Z-A)",
"all_rights_reserved": "© {{date}} <0>Linkwarden</0>. Всі права захищені.",
"you_have_no_collections": "У вас немає колекцій...",
"you_have_no_tags": "У вас немає міток...",
"cant_change_collection_you_dont_own": "Ви не можете вносити зміни в колекцію, яка вам не належить.",
"account": "Обліковий запис",
"billing": "Виставлення рахунків",
"linkwarden_version": "Linkwarden {{version}}",
"help": "Довідка",
"github": "GitHub",
"twitter": "Twitter",
"mastodon": "Mastodon",
"link_preservation_in_queue": "Збереження посилання наразі в черзі",
"check_back_later": "Перевірте пізніше, щоб побачити результат",
"there_are_more_formats": "У черзі більше збережених форматів",
"settings": "Налаштування",
"switch_to": "Перемикнути на {{theme}} тему",
"logout": "Вийти",
"start_journey": "Почніть свою подорож, створивши нове посилання!",
"create_new_link": "Створити нове посилання",
"new_link": "Нове посилання",
"create_new": "Створити новий...",
"pwa_install_prompt": "Установіть Linkwarden на головний екран для швидшого доступу та покращеного досвіду. <0>Докладніше</0>",
"full_content": "Повний вміст",
"slower": "повільніше",
"new_version_announcement": "Подивіться, що нового в <0>Linkwarden {{version}}!</0>",
"creating": "Створення...",
"upload_file": "Завантажте файл",
"file": "Файл",
"file_types": "PDF, PNG, JPG (до {{size}} МБ)",
"description": "Опис",
"auto_generated": "Буде створено автоматично, якщо нічого не надано.",
"example_link": "напр. Приклад посилання",
"hide": "Приховувати",
"more": "Більше",
"options": "Параметри",
"description_placeholder": "Нотатки, думки тощо.",
"deleting": "Видалення...",
"token_revoked": "Жетон відкликано.",
"revoke_token": "Відкликати жетон",
"revoke_confirmation": "Ви впевнені, що бажаєте відкликати цей жетон доступу? Будь-які програми чи служби, що використовують цей жетон, більше не зможуть отримати доступ до Linkwarden за допомогою нього.",
"revoke": "Відкликати",
"sending_request": "Надсилання запиту...",
"link_being_archived": "Посилання архівується...",
"preserved_formats": "Збережені формати",
"available_formats": "Для цього посилання доступні такі формати",
"readable": "Читабельний",
"preservation_in_queue": "У черзі збереження посилання",
"view_latest_snapshot": "Перегляньте останній знімок на archive.org",
"refresh_preserved_formats": "Оновити збережені формати",
"this_deletes_current_preservations": "Це видаляє поточні збереження",
"create_new_user": "Створити нового користувача",
"placeholder_johnny": "Микола",
"placeholder_email": "mykola@ukr.net",
"placeholder_john": "mykola",
"user_created": "Користувач створений!",
"fill_all_fields_error": "Будь ласка, заповніть усі поля.",
"password_change_note": "<0>Примітка:</0> переконайтеся, що ви повідомили користувача, що йому потрібно змінити свій пароль.",
"create_user": "Створити користувача",
"creating_token": "Створення жетона...",
"token_created": "Жетон створено!",
"access_token_created": "Жетон доступу створено",
"token_creation_notice": "Ваш новий жетон створено. Будь ласка, скопіюйте його та зберігайте в безпечному місці. Ви не зможете побачити це знову.",
"copied_to_clipboard": "Скопійовано в буфер обміну!",
"copy_to_clipboard": "Копіювати в буфер обміну",
"create_access_token": "Створіть жетон доступу",
"expires_in": "Термін дії закінчується через",
"token_name_placeholder": "напр. Для ярлика iOS",
"create_token": "Створити жетон доступу",
"7_days": "7 днів",
"30_days": "30 днів",
"60_days": "60 днів",
"90_days": "90 днів",
"no_expiration": "Без терміну дії",
"creating_link": "Створення посилання...",
"link_created": "Посилання створено!",
"link_name_placeholder": "Буде створено автоматично, якщо залишити поле пустим.",
"link_url_placeholder": "напр. http://example.com/",
"link_description_placeholder": "Нотатки, думки тощо.",
"more_options": "Більш параметрів",
"hide_options": "Сховати параметри",
"create_link": "Створити посилання",
"new_sub_collection": "Нова підколекція",
"for_collection": "Для {{name}}",
"create_new_collection": "Створити нову колекцію",
"color": "Колір",
"reset_defaults": "Скинути до значень за замовчуванням",
"updating_collection": "Оновлення колекції...",
"collection_name_placeholder": "напр. Збірка прикладів",
"collection_description_placeholder": "Мета цієї збірки...",
"create_collection_button": "Створити колекцію",
"password_change_warning": "Перш ніж змінювати адресу електронної скриньки, підтвердьте свій пароль.",
"stripe_update_note": " Оновлення цього поля також змінить вашу платіжну електронну скриньку на сервісі Stripe.",
"sso_will_be_removed_warning": "Якщо ви зміните адресу електронної скриньки, усі теперішні з’єднання SSO {{service}} буде видалено.",
"old_email": "Стара електронна скринька",
"new_email": "Нова електронна скринька",
"confirm": "Підтвердити",
"edit_link": "Редагувати посилання",
"updating": "Оновлення...",
"updated": "Оновлено!",
"placeholder_example_link": "напр. Приклад посилання",
"make_collection_public": "Зробити колекцію публічною",
"make_collection_public_checkbox": "Зробити це загальнодоступною колекцією",
"make_collection_public_desc": "Це дозволить будь-кому переглядати цю колекцію та її користувачів.",
"sharable_link": "Посилання для спільного використання",
"copied": "Скопійовано!",
"members": "Члени",
"members_username_placeholder": "Ім'я користувача (без '@')",
"owner": "Власник",
"admin": "Адмін",
"contributor": "Дописувач",
"viewer": "Переглядач",
"viewer_desc": "Доступ лише для читання",
"contributor_desc": "Може переглядати та створювати посилання",
"admin_desc": "Повний доступ до всіх посилань",
"remove_member": "Видалити учасника",
"placeholder_example_collection": "напр. Збірка прикладів",
"placeholder_collection_purpose": "Мета цієї колекції...",
"deleting_user": "Видалення...",
"user_deleted": "Користувача видалено.",
"delete_user": "Видалити користувача",
"confirm_user_deletion": "Ви впевнені, що хочете видалити цього користувача?",
"irreversible_action_warning": "Це незворотна дія!",
"delete_confirmation": "Видаліть, я знаю, що роблю",
"delete_link": "Видалити посилання",
"deleted": "Видалено.",
"link_deletion_confirmation_message": "Ви впевнені, що хочете видалити це посилання?",
"warning": "Попередження",
"irreversible_warning": "Це незворотна дія!",
"shift_key_tip": "Утримуйте клавішу Shift під час натискання 'Видалити', щоб обійти це підтвердження в майбутньому.",
"deleting_collection": "Видалення...",
"collection_deleted": "Колекцію видалено.",
"confirm_deletion_prompt": "Щоб підтвердити, введіть \"{{name}}\" у полі нижче:",
"type_name_placeholder": "Введіть \"{{name}}\" тут.",
"deletion_warning": "Видалення цієї колекції призведе до остаточного видалення всього її вмісту, і вона стане недоступною для всіх, включаючи учасників з попереднім доступом.",
"leave_prompt": "Натисніть кнопку нижче, щоб залишити поточну колекцію.",
"leave": "Залишити",
"edit_links": "Редагувати {{count}} посилання",
"move_to_collection": "Перейти до колекції",
"add_tags": "Додайте мітки",
"remove_previous_tags": "Видалити попередні мітки",
"delete_links": "Видалити {{count}} посилання",
"links_deletion_confirmation_message": "Ви впевнені, що хочете видалити ці посилання? ",
"warning_irreversible": "Увага: Це незворотна дія!",
"shift_key_instruction": "Утримуйте клавішу 'Shift' під час натискання 'Видалити', щоб обійти це підтвердження в майбутньому.",
"link_selection_error": "У вас немає дозволу редагувати або видаляти цей елемент.",
"no_description": "Опис не надано.",
"applying": "Застосування...",
"unpin": "Відкріпити",
"pin_to_dashboard": "Закріплення на інформаційній панелі",
"show_link_details": "Показати деталі посилання",
"link_pinned": "Посилання закріплено!",
"link_unpinned": "Посилання відкріплене!",
"webpage": "Вебсторінка",
"server_administration": "Адміністрування сервера",
"all_collections": "Всі колекції",
"dashboard": "Інформаційна панелі",
"demo_title": "Тільки демонстрація",
"demo_desc": "Це лише демонстраційний екземпляр Linkwarden, і завантаження вимкнено.",
"demo_desc_2": "Якщо ви хочете спробувати повну версію, ви можете підписатися на безплатну пробну версію за посиланням:",
"demo_button": "Увійдіть як демокористувач",
"regular": "Звичний",
"thin": "Тонкий",
"bold": "Жирний",
"fill": "Заповнений",
"duotone": "У двоколірному режимі",
"light_icon": "Світлий",
"search": "Пошук",
"set_custom_icon": "Встановити користувальницьку піктограму",
"view": "Переглянути",
"show": "Показати",
"image": "Зображення",
"icon": "Піктограма",
"date": "Дата",
"preview_unavailable": "Попередній перегляд недоступний",
"saved": "Збережено",
"untitled": "Без назви",
"no_tags": "Без міток.",
"no_description_provided": "Опис не надано.",
"change_icon": "Змінити піктограму",
"upload_preview_image": "Завантажте зображення для попереднього перегляду",
"columns": "Стовпці",
"default": "За замовчуванням"
}

View File

@@ -0,0 +1,374 @@
{
"user_administration": "使用者管理",
"search_users": "搜尋使用者",
"no_users_found": "未找到使用者。",
"no_user_found_in_search": "在給定的搜尋查詢中未找到使用者。",
"username": "使用者名稱",
"email": "電子郵件",
"subscribed": "訂閱",
"created_at": "建立時間",
"not_available": "N/A",
"check_your_email": "檢查你的電子郵件",
"authenticating": "身份驗證中...",
"verification_email_sent": "發送驗證郵件。",
"verification_email_sent_desc": "登入連結已發送到您的電子郵件信箱。如果沒有看到郵件,請檢查你的垃圾郵件資料夾。",
"resend_email": "重新發送電子郵件",
"invalid_credentials": "無效憑證",
"fill_all_fields": "請填寫所有欄位",
"enter_credentials": "帳號登入",
"username_or_email": "使用者名稱或電子郵件",
"password": "密碼",
"confirm_password": "確認密碼",
"forgot_password": "忘記密碼?",
"login": "登入",
"or_continue_with": "使用其他方式登入",
"new_here": "新用戶?",
"sign_up": "註冊",
"sign_in_to_your_account": "登入你的帳戶",
"dashboard_desc": "您的數據概覽",
"link": "連結",
"links": "連結",
"collection": "收藏夾",
"collections": "收藏夾",
"tag": "標籤",
"tags": "標籤",
"recent": "最近",
"recent_links_desc": "最近新增的連結",
"view_all": "查看全部",
"view_added_links_here": "查看最近新增的連結!",
"view_added_links_here_desc": "此部分將顯示您有權存取的每個收藏夾中最新新增的連結。",
"add_link": "新增連結",
"import_links": "匯入連結",
"from_linkwarden": "從 Linkwarden",
"from_html": "從書籤 HTML 檔案",
"from_wallabag": "從 WallabagJSON 檔案)",
"pinned": "釘選",
"pinned_links_desc": "您釘選的連結",
"pin_favorite_links_here": "在這裡釘選您最喜歡的連結!",
"pin_favorite_links_here_desc": "您可以透過點擊每個連結上的三個點,然後點擊「釘選到儀表板」來釘選您最喜歡的連結。",
"sending_password_link": "發送重設密碼連結",
"password_email_prompt": "輸入您的電子郵件,我們會給您發送一個重設密碼的連結。",
"send_reset_link": "發送重設連結",
"reset_email_sent_desc": "檢查您的電子郵件以取得重設密碼的連結。如果它在幾分鐘內沒有出現,請檢查您的垃圾郵件資料夾。",
"back_to_login": "返回登入",
"email_sent": "郵件已發送!",
"passwords_mismatch": "密碼錯誤。",
"password_too_short": "密碼需至少 8 個字元。",
"creating_account": "正在建立帳戶...",
"account_created": "帳戶建立成功!",
"trial_offer_desc": "免費解鎖 {{count}} 天的高級服務!",
"register_desc": "建立新帳戶",
"registration_disabled_desc": "此系統已停用註冊,如有任何問題,請聯絡管理員。",
"enter_details": "輸入您的詳細資訊",
"display_name": "顯示名稱",
"sign_up_agreement": "註冊即表示您同意遵守我們的 <0>服務條款</0> 和 <1>隱私政策</1>。",
"need_help": "需要幫助嗎?",
"get_in_touch": "聯絡我們",
"already_registered": "已經有帳戶?",
"deleting_selections": "正在刪除選取的連結...",
"links_deleted": "已刪除 {{count}} 個連結。",
"link_deleted": "已刪除 1 個連結。",
"links_selected": "已選取 {{count}} 個連結。",
"link_selected": "已選取 1 個連結。",
"nothing_selected": "未選取任何內容",
"edit": "編輯",
"delete": "刪除",
"nothing_found": "沒有找到任何內容。",
"redirecting_to_stripe": "正在重新導向到 Stripe...",
"subscribe_title": "訂閱 Linkwarden",
"subscribe_desc": "您將跳轉到 Stripe如有任何問題請隨時聯繫我們 <0>support@linkwarden.app</0>。",
"monthly": "每月",
"yearly": "每年",
"discount_percent": "{{percent}}% 折扣",
"billed_monthly": "每月計費",
"billed_yearly": "每年計費",
"total": "總計",
"total_annual_desc": "{{count}} 天免費體驗,然後每年 ${{annualPrice}}",
"total_monthly_desc": "{{count}} 天免費體驗,然後每月 ${{monthlyPrice}}",
"plus_tax": "+稅金(如適用)",
"complete_subscription": "完成訂閱",
"sign_out": "登出",
"access_tokens": "Access Tokens",
"access_tokens_description": "Access Tokens 可用於從其他應用程式和服務存取 Linkwarden而無需洩露您的使用者名稱和密碼。",
"new_token": "新 Access Tokens",
"name": "名稱",
"created_success": "建立成功!",
"created": "已建立",
"expires": "過期",
"accountSettings": "帳戶設定",
"language": "語言",
"profile_photo": "頭像",
"upload_new_photo": "上傳新頭像",
"remove_photo": "移除照片",
"make_profile_private": "將個人資料設為私人",
"profile_privacy_info": "這將限制誰可以找到您並將您添加到新收藏夾。",
"whitelisted_users": "白名單使用者",
"whitelisted_users_info": "請提供您希望允許查看您個人資料的使用者名稱,以逗號分隔。",
"whitelisted_users_placeholder": "您的資料現在對所有人都是隱藏的。",
"save_changes": "儲存變更",
"import_export": "匯入 & 匯出",
"import_data": "從其他平台匯入",
"download_data": "下載資料",
"export_data": "匯出資料",
"delete_account": "刪除帳戶",
"delete_account_warning": "這將永久刪除您擁有的所有連結、收藏夾、標籤和存檔資料。",
"cancel_subscription_notice": "這將取消您的訂閱。",
"account_deletion_page": "帳戶刪除頁面",
"applying_settings": "正在套用設定...",
"settings_applied": "設定已套用!",
"email_change_request": "已發送電子郵件變更請求。請確認新的電子郵件地址。",
"image_upload_size_error": "請選擇小於 1 MB 的 PNG 或 JPEG 檔案。",
"image_upload_format_error": "無效的檔案格式。",
"importing_bookmarks": "正在匯入書籤...",
"import_success": "書籤已匯入!請重新整理頁面。",
"more_coming_soon": "更多功能即將推出...",
"billing_settings": "帳單設定",
"manage_subscription_intro": "要管理/取消訂閱,請訪問",
"billing_portal": "帳單管理",
"help_contact_intro": "如果您仍然需要協助或遇到任何問題,請隨時與我們聯繫:",
"fill_required_fields": "請填寫必填欄位。",
"deleting_message": "正在刪除所有內容,請稍候...",
"delete_warning": "這將永久刪除您擁有的所有連結、收藏夾、標籤和存檔資料。它還將登出您的帳戶。此操作不可逆!",
"optional": "可選",
"feedback_help": "(但它確實能幫助我們改善!)",
"reason_for_cancellation": "取消原因",
"please_specify": "請指定",
"customer_service": "客戶服務",
"low_quality": "品質不佳",
"missing_features": "缺少功能",
"switched_service": "選擇服務",
"too_complex": "太複雜",
"too_expensive": "太昂貴",
"unused": "未使用",
"other": "其他",
"more_information": "更多資訊(越詳細,越有幫助)",
"feedback_placeholder": "例如,我需要一個功能...",
"delete_your_account": "刪除帳戶",
"change_password": "更改密碼",
"password_length_error": "密碼需至少 8 個字元。",
"applying_changes": "正在套用...",
"password_change_instructions": "要更改您的密碼,請填寫以下內容。您的密碼至少應為 8 個字元。",
"old_password": "舊密碼",
"new_password": "新密碼",
"preference": "偏好設定",
"select_theme": "選擇主題",
"dark": "深色",
"light": "淺色",
"archive_settings": "封存設定",
"formats_to_archive": "封存/保存網頁格式:",
"screenshot": "截圖",
"pdf": "PDF",
"archive_org_snapshot": "Archive.org 快照",
"link_settings": "連結設定",
"prevent_duplicate_links": "防止重複連結",
"clicking_on_links_should": "點擊連結:",
"open_original_content": "開啟原始內容",
"open_pdf_if_available": "開啟 PDF如果有",
"open_readable_if_available": "開啟可讀視圖(如果可用)",
"open_screenshot_if_available": "開啟截圖(如果有)",
"open_webpage_if_available": "開啟網頁副本(如果有)",
"tag_renamed": "標籤已重新命名!",
"tag_deleted": "標籤已刪除!",
"rename_tag": "重新命名標籤",
"delete_tag": "刪除標籤",
"list_created_with_linkwarden": "使用 Linkwarden 建立的清單",
"by_author": "作者 {{author}}",
"by_author_and_other": "作者:{{author}} 和其他 {{count}} 人",
"by_author_and_others": "作者:{{author}} 和其他 {{count}} 人",
"search_count_link": "搜尋 {{count}} 個連結",
"search_count_links": "搜尋 {{count}} 個連結",
"collection_is_empty": "此收藏夾為空",
"all_links": "所有連結",
"all_links_desc": "所有收藏夾的連結",
"you_have_not_added_any_links": "您還沒有新增任何連結",
"collections_you_own": "您的收藏夾",
"new_collection": "新收藏夾",
"other_collections": "其他收藏夾",
"other_collections_desc": "您所屬的共享收藏夾",
"showing_count_results": "顯示 {{count}} 筆結果",
"showing_count_result": "顯示 {{count}} 筆結果",
"edit_collection_info": "編輯收藏夾資訊",
"share_and_collaborate": "共享與協作",
"view_team": "查看團隊",
"team": "團隊",
"create_subcollection": "建立子收藏夾",
"delete_collection": "刪除收藏夾",
"leave_collection": "離開收藏夾",
"email_verified_signing_out": "電子郵件已驗證。正在登出...",
"invalid_token": "無效的 token",
"sending_password_recovery_link": "正在發送密碼重設連結...",
"please_fill_all_fields": "請填寫所有欄位。",
"password_updated": "密碼已更新!",
"reset_password": "重設密碼",
"enter_email_for_new_password": "輸入您的電子郵件,以便我們向您發送建立新密碼的連結。",
"update_password": "更新密碼",
"password_successfully_updated": "您的密碼已成功更新。",
"user_already_member": "使用者已存在。",
"you_are_already_collection_owner": "您已經是收藏夾的擁有者。",
"date_newest_first": "日期(最新)",
"date_oldest_first": "日期(最早)",
"name_az": "名稱A-Z",
"name_za": "名稱Z-A",
"description_az": "描述A-Z",
"description_za": "描述Z-A",
"all_rights_reserved": "© {{date}} <0>Linkwarden</0> 版權所有",
"you_have_no_collections": "您還沒有收藏夾...",
"you_have_no_tags": "您還沒有標籤...",
"cant_change_collection_you_dont_own": "您無法對不屬於您的收藏夾進行更改。",
"account": "帳戶",
"billing": "帳單",
"linkwarden_version": "Linkwarden {{version}}",
"help": "說明",
"github": "GitHub",
"twitter": "Twitter",
"mastodon": "Mastodon",
"link_preservation_in_queue": "連結保存正在處理中...",
"check_back_later": "請稍後再查看結果",
"there_are_more_formats": "隊列中有更多已保存的格式。",
"settings": "設定",
"switch_to": "切換到 {{theme}}",
"logout": "登出",
"start_journey": "透過建立新的連結開始您的旅程!",
"create_new_link": "建立新連結",
"new_link": "新連結",
"create_new": "新建",
"pwa_install_prompt": "安裝 Linkwarden 到您的主畫面,以獲得更快的存取和增強的體驗。<0>了解更多</0>",
"full_content": "完整內容",
"slower": "較慢",
"new_version_announcement": "查看 <0>Linkwarden {{version}}</0> 了解新功能。",
"creating": "正在建立...",
"upload_file": "上傳檔案",
"file": "檔案",
"file_types": "PDFPNGJPG不超過 {{size}} MB",
"description": "描述",
"auto_generated": "若未提供將自動產生。",
"example_link": "例如:範例連結",
"hide": "隱藏",
"more": "更多",
"options": "選項",
"description_placeholder": "筆記、想法等等。",
"deleting": "正在刪除...",
"token_revoked": "Token 已撤銷。",
"revoke_token": "撤銷 Token",
"revoke_confirmation": "確定要撤銷此 Access Token 嗎?任何使用此 Token 的應用程式或服務將無法再使用它存取 Linkwarden。",
"revoke": "撤銷",
"sending_request": "正在發送請求...",
"link_being_archived": "連結正在封存...",
"preserved_formats": "保存格式",
"available_formats": "以下格式可用於此連結",
"readable": "可讀視圖",
"preservation_in_queue": "連結保存正在處理中...",
"view_latest_snapshot": "在 archive.org 上查看最新快照",
"refresh_preserved_formats": "重新整理保存格式",
"this_deletes_current_preservations": "這將刪除目前的保存",
"create_new_user": "建立新使用者",
"placeholder_johnny": "Johnny",
"placeholder_email": "johnny@example.com",
"placeholder_john": "john",
"user_created": "使用者已建立",
"fill_all_fields_error": "請填寫所有欄位。",
"password_change_note": "<0>注意:</0>請務必通知使用者他們需要更改密碼。",
"create_user": "建立使用者",
"creating_token": "正在建立 Token...",
"token_created": "Token 建立完成",
"access_token_created": "Access Token 已建立",
"token_creation_notice": "您的新 Token 已建立。請複製並保存在安全的地方。您將無法再看到它。",
"copied_to_clipboard": "已複製到剪貼簿",
"copy_to_clipboard": "複製到剪貼簿",
"create_access_token": "建立一個 Access Token",
"expires_in": "有效期:",
"token_name_placeholder": "例如iOS 快捷方式",
"create_token": "建立 Access Token",
"7_days": "7 天",
"30_days": "30 天",
"60_days": "60 天",
"90_days": "90 天",
"no_expiration": "無期限",
"creating_link": "正在建立連結...",
"link_created": "連結已建立!",
"link_name_placeholder": "若留空將自動產生。",
"link_url_placeholder": "例如http://example.com/",
"link_description_placeholder": "筆記、想法等等。",
"more_options": "更多選項",
"hide_options": "隱藏選項",
"create_link": "建立連結",
"new_sub_collection": "新子收藏夾",
"for_collection": "針對 {{name}}",
"create_new_collection": "建立新收藏夾",
"color": "顏色",
"reset": "重設",
"collection_name_placeholder": "例如:範例收藏夾",
"collection_description_placeholder": "此收藏夾用於...",
"create_collection_button": "建立收藏夾",
"password_change_warning": "更改電子郵件前,請確認密碼。",
"stripe_update_note": " 更新此欄位也會更改 Stripe 上的帳單電子郵件。",
"sso_will_be_removed_warning": "如果您更改電子郵件地址,任何現有的 {{service}} SSO 連結都將被刪除。",
"old_email": "舊電子郵件",
"new_email": "新電子郵件",
"confirm": "確認",
"edit_link": "編輯連結",
"updating": "正在更新...",
"updated": "已更新",
"placeholder_example_link": "例如:範例連結",
"make_collection_public": "公開收藏夾",
"make_collection_public_checkbox": "將其設為公開收藏夾",
"make_collection_public_desc": "這將允許任何人查看該收藏夾及其使用者。",
"sharable_link_guide": "可分享的連結(點擊複製)",
"copied": "已複製",
"members": "成員",
"members_username_placeholder": "使用者名稱(不含 '@'",
"owner": "擁有者",
"admin": "管理員",
"contributor": "Contributor",
"viewer": "Viewer",
"viewer_desc": "唯讀存取",
"contributor_desc": "可以查看和建立連結",
"admin_desc": "完全存取所有連結",
"remove_member": "移除成員",
"placeholder_example_collection": "例如:範例收藏夾",
"placeholder_collection_purpose": "此收藏夾的目的...",
"deleting_user": "正在刪除使用者...",
"user_deleted": "使用者已刪除",
"delete_user": "刪除使用者",
"confirm_user_deletion": "確定要刪除此使用者嗎?",
"irreversible_action_warning": "此操作不可逆!",
"delete_confirmation": "刪除,我知道我在做什麼。",
"delete_link": "刪除連結",
"deleted": "已刪除",
"link_deletion_confirmation_message": "要刪除此連結嗎?",
"warning": "警告",
"irreversible_warning": "此操作不可逆!",
"shift_key_tip": "按住 Shift 鍵並點擊「刪除」即可在以後繞過此確認。",
"deleting_collection": "正在刪除...",
"collection_deleted": "收藏夾已刪除。",
"confirm_deletion_prompt": "請確認,在下面的輸入框中輸入 \"{{name}}\"",
"type_name_placeholder": "在此輸入 \"{{name}}\" 。",
"deletion_warning": "刪除此收藏夾將永久清除其所有內容,並且所有人都將無法存取,包括之前具有存取權限的成員。",
"leave_prompt": "點擊下面的按鈕離開當前收藏夾。",
"leave": "離開",
"edit_links": "編輯 {{count}} 個連結",
"move_to_collection": "移至收藏夾",
"add_tags": "新增標籤",
"remove_previous_tags": "刪除之前的標籤",
"delete_links": "刪除 {{count}} 個連結",
"links_deletion_confirmation_message": "確定要刪除 {{count}} 個連結嗎?",
"warning_irreversible": "警告:此操作不可逆!",
"shift_key_instruction": "按住「Shift」鍵並點擊「刪除」即可在以後繞過此確認。",
"link_selection_error": "您無權編輯或刪除此項目。",
"no_description": "未提供描述",
"applying": "正在套用...",
"unpin": "取消釘選",
"pin_to_dashboard": "釘選到儀表板",
"show_link_details": "顯示連結詳細資訊",
"hide_link_details": "隱藏連結詳細資訊",
"link_pinned": "連結已釘選!",
"link_unpinned": "連結已取消釘選!",
"webpage": "網頁",
"server_administration": "系統管理",
"all_collections": "所有收藏夾",
"dashboard": "儀表板",
"demo_title": "僅供展示",
"demo_desc": "這只是 Linkwarden 的展示實例,禁止上傳檔案。",
"demo_desc_2": "如果您想嘗試完整版,您可以註冊免費試用:",
"demo_button": "以展示用戶登入"
}

View File

@@ -255,7 +255,7 @@
"sending_request": "正在发送请求...",
"link_being_archived": "链接正在归档...",
"preserved_formats": "保存格式",
"available_formats": "以下格式可用于此链接",
"available_formats": "以下格式可用于此链接",
"readable": "可读视图",
"preservation_in_queue": "链接保存正在处理中...",
"view_latest_snapshot": "在 archive.org 上查看最新快照",

381
yarn.lock
View File

@@ -645,13 +645,6 @@
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.13.10":
version "7.23.8"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650"
integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2":
version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
@@ -1281,6 +1274,16 @@
resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.1.1.tgz#ab9cd8755d1976e72fc77a00f7655a64efe6cd5d"
integrity sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==
"@phosphor-icons/core@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@phosphor-icons/core/-/core-2.1.1.tgz#62a4cfbec9772f1a613a647da214fbb96f3ad39d"
integrity sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ==
"@phosphor-icons/react@^2.1.7":
version "2.1.7"
resolved "https://registry.yarnpkg.com/@phosphor-icons/react/-/react-2.1.7.tgz#b11a4b25849b7e3849970b688d9fe91e5d4fd8d7"
integrity sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==
"@pkgr/utils@^2.3.1":
version "2.3.1"
resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.3.1.tgz#0a9b06ffddee364d6642b3cd562ca76f55b34a03"
@@ -1300,164 +1303,167 @@
dependencies:
playwright "1.45.0"
"@prisma/client@^4.16.2":
version "4.16.2"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.16.2.tgz#3bb9ebd49b35c8236b3d468d0215192267016e2b"
integrity sha512-qCoEyxv1ZrQ4bKy39GnylE8Zq31IRmm8bNhNbZx7bF2cU5aiCCnSa93J2imF88MBjn7J9eUQneNxUQVJdl/rPQ==
"@prisma/client@^5.21.1":
version "5.21.1"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.21.1.tgz#ad51ef220eb80173f882e859960d81e626b73898"
integrity sha512-3n+GgbAZYjaS/k0M03yQsQfR1APbr411r74foknnsGpmhNKBG49VuUkxIU6jORgvJPChoD4WC4PqoHImN1FP0w==
"@prisma/debug@5.21.1":
version "5.21.1"
resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.21.1.tgz#df4383cb8a6273b1d6112cda0f1d5bef73e71be7"
integrity sha512-uY8SAhcnORhvgtOrNdvWS98Aq/nkQ9QDUxrWAgW8XrCZaI3j2X7zb7Xe6GQSh6xSesKffFbFlkw0c2luHQviZA==
"@prisma/engines-version@5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36":
version "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36.tgz#8a5f136a8ee71995bf635686bd2f1a6650f9320c"
integrity sha512-qvnEflL0//lh44S/T9NcvTMxfyowNeUxTunPcDfKPjyJNrCNf2F1zQLcUv5UHAruECpX+zz21CzsC7V2xAeM7Q==
"@prisma/engines@5.21.1":
version "5.21.1"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.21.1.tgz#05f9bc50eb4aa169b31cadfb402165bd44e0653f"
integrity sha512-hGVTldUkIkTwoV8//hmnAAiAchi4oMEKD3aW5H2RrnI50tTdwza7VQbTTAyN3OIHWlK5DVg6xV7X8N/9dtOydA==
dependencies:
"@prisma/engines-version" "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81"
"@prisma/debug" "5.21.1"
"@prisma/engines-version" "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36"
"@prisma/fetch-engine" "5.21.1"
"@prisma/get-platform" "5.21.1"
"@prisma/engines-version@4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81":
version "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz#d3b5dcf95b6d220e258cbf6ae19b06d30a7e9f14"
integrity sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==
"@prisma/engines@4.16.2":
version "4.16.2"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.16.2.tgz#5ec8dd672c2173d597e469194916ad4826ce2e5f"
integrity sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==
"@radix-ui/primitive@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd"
integrity sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==
"@prisma/fetch-engine@5.21.1":
version "5.21.1"
resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.21.1.tgz#c56008f954199a3f3f2183d892f093f64976e4d8"
integrity sha512-70S31vgpCGcp9J+mh/wHtLCkVezLUqe/fGWk3J3JWZIN7prdYSlr1C0niaWUyNK2VflLXYi8kMjAmSxUVq6WGQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@prisma/debug" "5.21.1"
"@prisma/engines-version" "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36"
"@prisma/get-platform" "5.21.1"
"@radix-ui/react-compose-refs@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"
integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==
"@prisma/get-platform@5.21.1":
version "5.21.1"
resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.21.1.tgz#a2219e7755cec881dffc66469c31bb0975a95b54"
integrity sha512-sRxjL3Igst3ct+e8ya/x//cDXmpLbZQ5vfps2N4tWl4VGKQAmym77C/IG/psSMsQKszc8uFC/q1dgmKFLUgXZQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@prisma/debug" "5.21.1"
"@radix-ui/react-context@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c"
integrity sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==
"@radix-ui/react-dialog@^1.0.4":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300"
integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==
"@radix-ui/react-compose-refs@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==
"@radix-ui/react-context@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a"
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
"@radix-ui/react-dialog@^1.1.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz#d9345575211d6f2d13e209e84aec9a8584b54d6c"
integrity sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-dismissable-layer" "1.0.5"
"@radix-ui/react-focus-guards" "1.0.1"
"@radix-ui/react-focus-scope" "1.0.4"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-portal" "1.0.4"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-dismissable-layer" "1.1.1"
"@radix-ui/react-focus-guards" "1.1.1"
"@radix-ui/react-focus-scope" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-portal" "1.1.2"
"@radix-ui/react-presence" "1.1.1"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
react-remove-scroll "2.6.0"
"@radix-ui/react-dismissable-layer@1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4"
integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==
"@radix-ui/react-dismissable-layer@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz#cbdcb739c5403382bdde5f9243042ba643883396"
integrity sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown" "1.0.3"
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-focus-guards@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad"
integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-focus-guards@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe"
integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==
"@radix-ui/react-focus-scope@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525"
integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==
"@radix-ui/react-focus-scope@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2"
integrity sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-id@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0"
integrity sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==
"@radix-ui/react-id@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed"
integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-portal@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15"
integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==
"@radix-ui/react-portal@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.2.tgz#51eb46dae7505074b306ebcb985bf65cc547d74e"
integrity sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-presence@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
integrity sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==
"@radix-ui/react-presence@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz#98aba423dba5e0c687a782c0669dcd99de17f9b1"
integrity sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-primitive@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0"
integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==
"@radix-ui/react-primitive@2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884"
integrity sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-slot@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab"
integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==
"@radix-ui/react-slot@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84"
integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-use-callback-ref@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"
integrity sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1"
integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==
"@radix-ui/react-use-controllable-state@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286"
integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==
"@radix-ui/react-use-controllable-state@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0"
integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755"
integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==
"@radix-ui/react-use-escape-keydown@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754"
integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399"
integrity sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==
"@rushstack/eslint-patch@^1.1.3":
version "1.2.0"
@@ -2084,6 +2090,13 @@
dependencies:
"@types/react" "*"
"@types/react-window@^1.8.8":
version "1.8.8"
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3"
integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@18.2.14":
version "18.2.14"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.14.tgz#fa7a6fecf1ce35ca94e74874f70c56ce88f7a127"
@@ -3369,7 +3382,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9:
fast-glob@^3.2.11, fast-glob@^3.2.9:
version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
@@ -3380,6 +3393,17 @@ fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-glob@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@@ -3533,6 +3557,11 @@ fsevents@2.3.2, fsevents@~2.3.2:
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
fsevents@2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -3553,6 +3582,11 @@ functions-have-names@^1.2.2:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
fuse.js@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-7.0.0.tgz#6573c9fcd4c8268e403b4fc7d7131ffcf99a9eb2"
integrity sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==
gauge@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
@@ -4253,10 +4287,10 @@ jimp@^0.22.10:
"@jimp/types" "^0.22.10"
regenerator-runtime "^0.13.3"
jiti@^1.18.2:
version "1.18.2"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd"
integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==
jiti@^1.21.0:
version "1.21.6"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268"
integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==
jose@^4.11.1, jose@^4.11.4, jose@^4.14.1:
version "4.14.4"
@@ -4461,7 +4495,7 @@ make-error@^1.1.1:
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
memoize-one@^5.0.4:
"memoize-one@>=3.1.1 <6", memoize-one@^5.0.4:
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
@@ -5149,12 +5183,14 @@ pretty-format@^3.8.0:
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==
prisma@^4.16.2:
version "4.16.2"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.16.2.tgz#469e0a0991c6ae5bcde289401726bb012253339e"
integrity sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==
prisma@^5.21.1:
version "5.21.1"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.21.1.tgz#3ffe4f4b60ea8df2e6d5f24f0cea090bcc5c0bd6"
integrity sha512-PB+Iqzld/uQBPaaw2UVIk84kb0ITsLajzsxzsadxxl54eaU5Gyl2/L02ysivHxK89t7YrfQJm+Ggk37uvM70oQ==
dependencies:
"@prisma/engines" "4.16.2"
"@prisma/engines" "5.21.1"
optionalDependencies:
fsevents "2.3.3"
process@^0.11.10:
version "0.11.10"
@@ -5306,20 +5342,20 @@ react-redux@^7.0.3:
prop-types "^15.7.2"
react-is "^17.0.2"
react-remove-scroll-bar@^2.3.3:
version "2.3.4"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9"
integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==
react-remove-scroll-bar@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c"
integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==
dependencies:
react-style-singleton "^2.2.1"
tslib "^2.0.0"
react-remove-scroll@2.5.5:
version "2.5.5"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==
react-remove-scroll@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz#fb03a0845d7768a4f1519a99fdb84983b793dc07"
integrity sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==
dependencies:
react-remove-scroll-bar "^2.3.3"
react-remove-scroll-bar "^2.3.6"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.0"
@@ -5364,6 +5400,14 @@ react-transition-group@^4.3.0:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-window@^1.8.10:
version "1.8.10"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03"
integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==
dependencies:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@@ -5861,20 +5905,20 @@ tailwind-merge@^2.3.0:
dependencies:
"@babel/runtime" "^7.24.1"
tailwindcss@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==
tailwindcss@^3.4.10:
version "3.4.10"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.10.tgz#70442d9aeb78758d1f911af29af8255ecdb8ffef"
integrity sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==
dependencies:
"@alloc/quick-lru" "^5.2.0"
arg "^5.0.2"
chokidar "^3.5.3"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.2.12"
fast-glob "^3.3.0"
glob-parent "^6.0.2"
is-glob "^4.0.3"
jiti "^1.18.2"
jiti "^1.21.0"
lilconfig "^2.1.0"
micromatch "^4.0.5"
normalize-path "^3.0.0"
@@ -6239,12 +6283,12 @@ v8-compile-cache-lib@^3.0.1:
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
vaul@^0.8.8:
version "0.8.8"
resolved "https://registry.yarnpkg.com/vaul/-/vaul-0.8.8.tgz#c5edc041825fdeaddf0a89e326abcc7ac7449a2d"
integrity sha512-Z9K2b90M/LtY/sRyM1yfA8Y4mHC/5WIqhO2u7Byr49r5LQXkLGdVXiehsnjtws9CL+DyknwTuRMJXlCOHTqg/g==
vaul@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.1.tgz#93aceaad16f7c53aacf28a2609b2dd43b5a91fa0"
integrity sha512-+ejzF6ffQKPcfgS7uOrGn017g39F8SO4yLPXbBhpC7a0H+oPqPna8f1BUfXaz8eU4+pxbQcmjxW+jWBSbxjaFg==
dependencies:
"@radix-ui/react-dialog" "^1.0.4"
"@radix-ui/react-dialog" "^1.1.1"
verror@1.10.0:
version "1.10.0"
@@ -6485,6 +6529,11 @@ zod@3.21.4:
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==
zod@^3.23.8:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
zustand@^4.3.8:
version "4.3.8"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.8.tgz#37113df8e9e1421b0be1b2dca02b49b76210e7c4"