Merge pull request #754 from linkwarden/feat/customizable-links
Feat/customizable links
This commit is contained in:
+2
-5
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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." });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user