Merge pull request #754 from linkwarden/feat/customizable-links

Feat/customizable links
This commit is contained in:
Daniel
2024-09-04 22:20:16 -04:00
committed by GitHub
50 changed files with 1843 additions and 1200 deletions
+2 -5
View File
@@ -88,13 +88,10 @@ function App({
{icon}
<span data-testid="toast-message">{message}</span>
{t.type !== "loading" && (
<button
className="btn btn-xs outline-none btn-circle btn-ghost"
<div
data-testid="close-toast-button"
onClick={() => toast.dismiss(t.id)}
>
<i className="bi bi-x"></i>
</button>
></div>
)}
</div>
)}
+91 -4
View File
@@ -105,8 +105,6 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
response: "Collection is not accessible.",
});
// await uploadHandler(linkId, )
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER || 30000);
const numberOfLinksTheUserHas = await prisma.link.count({
@@ -119,8 +117,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if (numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
return res.status(400).json({
response:
"Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.",
response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
});
const NEXT_PUBLIC_MAX_FILE_BUFFER = Number(
@@ -208,4 +205,94 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
});
});
}
// To update the link preview
else if (req.method === "PUT" && format === ArchivedFormat.jpeg) {
if (process.env.NEXT_PUBLIC_DEMO === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const user = await verifyUser({ req, res });
if (!user) return;
const collectionPermissions = await getPermission({
userId: user.id,
linkId,
});
if (!collectionPermissions)
return res.status(400).json({
response: "Collection is not accessible.",
});
const memberHasAccess = collectionPermissions.members.some(
(e: UsersAndCollections) => e.userId === user.id && e.canCreate
);
if (!(collectionPermissions.ownerId === user.id || memberHasAccess))
return res.status(400).json({
response: "Collection is not accessible.",
});
const NEXT_PUBLIC_MAX_FILE_BUFFER = Number(
process.env.NEXT_PUBLIC_MAX_FILE_BUFFER || 10
);
const form = formidable({
maxFields: 1,
maxFiles: 1,
maxFileSize: NEXT_PUBLIC_MAX_FILE_BUFFER * 1024 * 1024,
});
form.parse(req, async (err, fields, files) => {
const allowedMIMETypes = ["image/png", "image/jpg", "image/jpeg"];
if (
err ||
!files.file ||
!files.file[0] ||
!allowedMIMETypes.includes(files.file[0].mimetype || "")
) {
// Handle parsing error
return res.status(400).json({
response: `Sorry, we couldn't process your file. Please ensure it's a PDF, PNG, or JPG format and doesn't exceed ${NEXT_PUBLIC_MAX_FILE_BUFFER}MB.`,
});
} else {
const fileBuffer = fs.readFileSync(files.file[0].filepath);
if (
Buffer.byteLength(fileBuffer) >
1024 * 1024 * Number(NEXT_PUBLIC_MAX_FILE_BUFFER)
)
return res.status(400).json({
response: `Sorry, we couldn't process your file. Please ensure it's a PNG, or JPG format and doesn't exceed ${NEXT_PUBLIC_MAX_FILE_BUFFER}MB.`,
});
const linkStillExists = await prisma.link.update({
where: { id: linkId },
data: {
updatedAt: new Date(),
},
});
if (linkStillExists) {
const collectionId = collectionPermissions.id;
createFolder({
filePath: `archives/preview/${collectionId}`,
});
generatePreview(fileBuffer, collectionId, linkId);
}
fs.unlinkSync(files.file[0].filepath);
if (linkStillExists)
return res.status(200).json({
response: linkStillExists,
});
else return res.status(400).json({ response: "Link not found." });
}
});
}
}
+17 -4
View File
@@ -24,6 +24,8 @@ import { useCollections } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import { useLinks } from "@/hooks/store/links";
import Links from "@/components/LinkViews/Links";
import Icon from "@/components/Icon";
import { IconWeight } from "@phosphor-icons/react";
export default function Index() {
const { t } = useTranslation();
@@ -110,10 +112,21 @@ export default function Index() {
{activeCollection && (
<div className="flex gap-3 items-start justify-between">
<div className="flex items-center gap-2">
<i
className="bi-folder-fill text-3xl drop-shadow"
style={{ color: activeCollection?.color }}
></i>
{activeCollection.icon ? (
<Icon
icon={activeCollection.icon}
size={45}
weight={
(activeCollection.iconWeight || "regular") as IconWeight
}
color={activeCollection.color}
/>
) : (
<i
className="bi-folder-fill text-3xl"
style={{ color: activeCollection.color }}
></i>
)}
<p className="sm:text-3xl text-2xl capitalize w-full py-1 break-words hyphens-auto font-thin">
{activeCollection?.name}
+4 -3
View File
@@ -17,11 +17,12 @@ const Index = () => {
}, []);
return (
<div className="flex h-screen py-20">
<div className="flex h-screen">
{getLink.data ? (
<LinkDetails
link={getLink.data}
className="max-w-xl p-5 m-auto w-full"
activeLink={getLink.data}
className="sm:max-w-xl sm:m-auto sm:p-5 w-full"
standalone
/>
) : (
<div className="max-w-xl p-5 m-auto w-full flex flex-col items-center gap-5">
+5 -6
View File
@@ -1,15 +1,13 @@
import LinkDetails from "@/components/LinkDetails";
import { useGetLink } from "@/hooks/store/links";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import getServerSideProps from "@/lib/client/getServerSideProps";
const Index = () => {
const router = useRouter();
const { id } = router.query;
useState;
const getLink = useGetLink();
useEffect(() => {
@@ -17,11 +15,12 @@ const Index = () => {
}, []);
return (
<div className="flex h-screen py-20">
<div className="flex h-screen">
{getLink.data ? (
<LinkDetails
link={getLink.data}
className="max-w-xl p-5 m-auto w-full"
activeLink={getLink.data}
className="sm:max-w-xl sm:m-auto sm:p-5 w-full"
standalone
/>
) : (
<div className="max-w-xl p-5 m-auto w-full flex flex-col items-center gap-5">
+23 -25
View File
@@ -146,31 +146,29 @@ export default function Index() {
<i className={"bi-hash text-primary text-3xl"} />
{renameTag ? (
<>
<form onSubmit={submit} className="flex items-center gap-2">
<input
type="text"
autoFocus
className="sm:text-3xl text-2xl bg-transparent h-10 w-3/4 outline-none border-b border-b-neutral-content"
value={newTagName}
onChange={(e) => setNewTagName(e.target.value)}
/>
<div
onClick={() => submit()}
id="expand-dropdown"
className="btn btn-ghost btn-square btn-sm"
>
<i className={"bi-check text-neutral text-2xl"}></i>
</div>
<div
onClick={() => cancelUpdateTag()}
id="expand-dropdown"
className="btn btn-ghost btn-square btn-sm"
>
<i className={"bi-x text-neutral text-2xl"}></i>
</div>
</form>
</>
<form onSubmit={submit} className="flex items-center gap-2">
<input
type="text"
autoFocus
className="sm:text-3xl text-2xl bg-transparent h-10 w-3/4 outline-none border-b border-b-neutral-content"
value={newTagName}
onChange={(e) => setNewTagName(e.target.value)}
/>
<div
onClick={() => submit()}
id="expand-dropdown"
className="btn btn-ghost btn-square btn-sm"
>
<i className={"bi-check2 text-neutral text-2xl"}></i>
</div>
<div
onClick={() => cancelUpdateTag()}
id="expand-dropdown"
className="btn btn-ghost btn-square btn-sm"
>
<i className={"bi-x text-neutral text-2xl"}></i>
</div>
</form>
) : (
<>
<p className="sm:text-3xl text-2xl capitalize">