added read-only mode + visual improvements

This commit is contained in:
daniel31x13 2024-07-16 20:33:33 -04:00
parent 6d30912812
commit 9c5226ee51
25 changed files with 172 additions and 16 deletions

View File

@ -21,6 +21,7 @@ BROWSER_TIMEOUT=
IGNORE_UNAUTHORIZED_CA= IGNORE_UNAUTHORIZED_CA=
IGNORE_HTTPS_ERRORS= IGNORE_HTTPS_ERRORS=
IGNORE_URL_SIZE_LIMIT= IGNORE_URL_SIZE_LIMIT=
DEMO_MODE=
NEXT_PUBLIC_ADMIN= NEXT_PUBLIC_ADMIN=
NEXT_PUBLIC_MAX_FILE_BUFFER= NEXT_PUBLIC_MAX_FILE_BUFFER=
MONOLITH_MAX_BUFFER= MONOLITH_MAX_BUFFER=

View File

@ -81,12 +81,15 @@ const LinkListOptions = ({
toast.dismiss(load); toast.dismiss(load);
response.ok && if (response.ok) {
toast.success( toast.success(
selectedLinks.length === 1 selectedLinks.length === 1
? t("link_deleted") ? t("link_deleted")
: t("links_deleted", { count: selectedLinks.length }) : t("links_deleted", { count: selectedLinks.length })
); );
} else {
toast.error(response.data as string);
}
}; };
return ( return (

View File

@ -55,8 +55,11 @@ export default function LinkActions({
toast.dismiss(load); toast.dismiss(load);
response.ok && if (response.ok) {
toast.success(isAlreadyPinned ? t("link_unpinned") : t("link_unpinned")); toast.success(isAlreadyPinned ? t("link_unpinned") : t("link_unpinned"));
} else {
toast.error(response.data as string);
}
}; };
const deleteLink = async () => { const deleteLink = async () => {
@ -66,7 +69,11 @@ export default function LinkActions({
toast.dismiss(load); toast.dismiss(load);
response.ok && toast.success(t("deleted")); if (response.ok) {
toast.success(t("deleted"));
} else {
toast.error(response.data as string);
}
}; };
return ( return (

View File

@ -157,7 +157,12 @@ export default function LinkCardCompact({
// linkInfo={showInfo} // linkInfo={showInfo}
/> />
</div> </div>
<div className="divider my-0 last:hidden h-[1px]"></div> <div
className="last:hidden rounded-none"
style={{
borderTop: "1px solid var(--fallback-bc,oklch(var(--bc)/0.1))",
}}
></div>
</> </>
); );
} }

View File

@ -30,7 +30,11 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
toast.dismiss(load); toast.dismiss(load);
response.ok && toast.success(t("deleted")); if (response.ok) {
toast.success(t("deleted"));
} else {
toast.error(response.data as string);
}
if (router.pathname.startsWith("/links/[id]")) { if (router.pathname.startsWith("/links/[id]")) {
router.push("/dashboard"); router.push("/dashboard");

View File

@ -20,7 +20,11 @@ export default function DeleteUserModal({ onClose, userId }: Props) {
toast.dismiss(load); toast.dismiss(load);
response.ok && toast.success(t("user_deleted")); if (response.ok) {
toast.success(t("user_deleted"));
} else {
toast.error(response.data as string);
}
onClose(); onClose();
}; };

View File

@ -30,6 +30,8 @@ export default function DeleteTokenModal({ onClose, activeToken }: Props) {
if (response.ok) { if (response.ok) {
toast.success(t("token_revoked")); toast.success(t("token_revoked"));
} else {
toast.error(response.data as string);
} }
onClose(); onClose();

View File

@ -12,7 +12,7 @@ export default function PageHeader({
return ( return (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<i <i
className={`${icon} text-primary text-3xl sm:text-4xl drop-shadow`} className={`${icon} text-primary sm:text-3xl text-2xl drop-shadow`}
></i> ></i>
<div> <div>
<p className="text-3xl capitalize font-thin">{title}</p> <p className="text-3xl capitalize font-thin">{title}</p>

View File

@ -77,6 +77,12 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
return res.send(file); return res.send(file);
} }
} else if (req.method === "POST") { } else if (req.method === "POST") {
if (process.env.DEMO_MODE === "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 }); const user = await verifyUser({ req, res });
if (!user) return; if (!user) return;
@ -86,14 +92,18 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
}); });
if (!collectionPermissions) if (!collectionPermissions)
return { response: "Collection is not accessible.", status: 400 }; return res.status(400).json({
response: "Collection is not accessible.",
});
const memberHasAccess = collectionPermissions.members.some( const memberHasAccess = collectionPermissions.members.some(
(e: UsersAndCollections) => e.userId === user.id && e.canCreate (e: UsersAndCollections) => e.userId === user.id && e.canCreate
); );
if (!(collectionPermissions.ownerId === user.id || memberHasAccess)) if (!(collectionPermissions.ownerId === user.id || memberHasAccess))
return { response: "Collection is not accessible.", status: 400 }; return res.status(400).json({
response: "Collection is not accessible.",
});
// await uploadHandler(linkId, ) // await uploadHandler(linkId, )
@ -108,10 +118,10 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
}); });
if (numberOfLinksTheUserHas > MAX_LINKS_PER_USER) if (numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
return { return res.status(400).json({
response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`, response:
status: 400, "Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.",
}; });
const NEXT_PUBLIC_MAX_FILE_BUFFER = Number( const NEXT_PUBLIC_MAX_FILE_BUFFER = Number(
process.env.NEXT_PUBLIC_MAX_FILE_BUFFER || 10 process.env.NEXT_PUBLIC_MAX_FILE_BUFFER || 10

View File

@ -7,6 +7,12 @@ export default async function forgotPassword(
res: NextApiResponse res: NextApiResponse
) { ) {
if (req.method === "POST") { if (req.method === "POST") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const email = req.body.email; const email = req.body.email;
if (!email) { if (!email) {

View File

@ -7,6 +7,12 @@ export default async function resetPassword(
res: NextApiResponse res: NextApiResponse
) { ) {
if (req.method === "POST") { if (req.method === "POST") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const token = req.body.token; const token = req.body.token;
const password = req.body.password; const password = req.body.password;

View File

@ -7,6 +7,12 @@ export default async function verifyEmail(
res: NextApiResponse res: NextApiResponse
) { ) {
if (req.method === "POST") { if (req.method === "POST") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const token = req.query.token; const token = req.query.token;
if (!token || typeof token !== "string") { if (!token || typeof token !== "string") {

View File

@ -19,9 +19,21 @@ export default async function collections(
.status(collections.status) .status(collections.status)
.json({ response: collections.response }); .json({ response: collections.response });
} else if (req.method === "PUT") { } else if (req.method === "PUT") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const updated = await updateCollectionById(user.id, collectionId, req.body); const updated = await updateCollectionById(user.id, collectionId, req.body);
return res.status(updated.status).json({ response: updated.response }); return res.status(updated.status).json({ response: updated.response });
} else if (req.method === "DELETE") { } else if (req.method === "DELETE") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const deleted = await deleteCollectionById(user.id, collectionId); const deleted = await deleteCollectionById(user.id, collectionId);
return res.status(deleted.status).json({ response: deleted.response }); return res.status(deleted.status).json({ response: deleted.response });
} }

View File

@ -16,6 +16,12 @@ export default async function collections(
.status(collections.status) .status(collections.status)
.json({ response: collections.response }); .json({ response: collections.response });
} else if (req.method === "POST") { } else if (req.method === "POST") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const newCollection = await postCollection(req.body, user.id); const newCollection = await postCollection(req.body, user.id);
return res return res
.status(newCollection.status) .status(newCollection.status)

View File

@ -29,6 +29,12 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
}); });
if (req.method === "PUT") { if (req.method === "PUT") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
if ( if (
link?.lastPreserved && link?.lastPreserved &&
getTimezoneDifferenceInMinutes(new Date(), link?.lastPreserved) < getTimezoneDifferenceInMinutes(new Date(), link?.lastPreserved) <

View File

@ -14,6 +14,12 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
response: updated.response, response: updated.response,
}); });
} else if (req.method === "PUT") { } else if (req.method === "PUT") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const updated = await updateLinkById( const updated = await updateLinkById(
user.id, user.id,
Number(req.query.id), Number(req.query.id),
@ -23,6 +29,12 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
response: updated.response, response: updated.response,
}); });
} else if (req.method === "DELETE") { } else if (req.method === "DELETE") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const deleted = await deleteLinkById(user.id, Number(req.query.id)); const deleted = await deleteLinkById(user.id, Number(req.query.id));
return res.status(deleted.status).json({ return res.status(deleted.status).json({
response: deleted.response, response: deleted.response,

View File

@ -37,11 +37,23 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
const links = await getLinks(user.id, convertedData); const links = await getLinks(user.id, convertedData);
return res.status(links.status).json({ response: links.response }); return res.status(links.status).json({ response: links.response });
} else if (req.method === "POST") { } else if (req.method === "POST") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const newlink = await postLink(req.body, user.id); const newlink = await postLink(req.body, user.id);
return res.status(newlink.status).json({ return res.status(newlink.status).json({
response: newlink.response, response: newlink.response,
}); });
} else if (req.method === "PUT") { } else if (req.method === "PUT") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const updated = await updateLinks( const updated = await updateLinks(
user.id, user.id,
req.body.links, req.body.links,
@ -52,6 +64,12 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
response: updated.response, response: updated.response,
}); });
} else if (req.method === "DELETE") { } else if (req.method === "DELETE") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const deleted = await deleteLinksById(user.id, req.body.linkIds); const deleted = await deleteLinksById(user.id, req.body.linkIds);
return res.status(deleted.status).json({ return res.status(deleted.status).json({
response: deleted.response, response: deleted.response,

View File

@ -30,6 +30,12 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
.status(data.status) .status(data.status)
.json(data.response); .json(data.response);
} else if (req.method === "POST") { } else if (req.method === "POST") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const request: MigrationRequest = JSON.parse(req.body); const request: MigrationRequest = JSON.parse(req.body);
let data; let data;

View File

@ -10,9 +10,21 @@ export default async function tags(req: NextApiRequest, res: NextApiResponse) {
const tagId = Number(req.query.id); const tagId = Number(req.query.id);
if (req.method === "PUT") { if (req.method === "PUT") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const tags = await updeteTagById(user.id, tagId, req.body); const tags = await updeteTagById(user.id, tagId, req.body);
return res.status(tags.status).json({ response: tags.response }); return res.status(tags.status).json({ response: tags.response });
} else if (req.method === "DELETE") { } else if (req.method === "DELETE") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const tags = await deleteTagById(user.id, tagId); const tags = await deleteTagById(user.id, tagId);
return res.status(tags.status).json({ response: tags.response }); return res.status(tags.status).json({ response: tags.response });
} }

View File

@ -7,6 +7,12 @@ export default async function token(req: NextApiRequest, res: NextApiResponse) {
if (!user) return; if (!user) return;
if (req.method === "DELETE") { if (req.method === "DELETE") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const deleted = await deleteToken(user.id, Number(req.query.id) as number); const deleted = await deleteToken(user.id, Number(req.query.id) as number);
return res.status(deleted.status).json({ response: deleted.response }); return res.status(deleted.status).json({ response: deleted.response });
} }

View File

@ -11,6 +11,12 @@ export default async function tokens(
if (!user) return; if (!user) return;
if (req.method === "POST") { if (req.method === "POST") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const token = await postToken(JSON.parse(req.body), user.id); const token = await postToken(JSON.parse(req.body), user.id);
return res.status(token.status).json({ response: token.response }); return res.status(token.status).json({ response: token.response });
} else if (req.method === "GET") { } else if (req.method === "GET") {

View File

@ -58,9 +58,21 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
} }
if (req.method === "PUT") { if (req.method === "PUT") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const updated = await updateUserById(userId, req.body); const updated = await updateUserById(userId, req.body);
return res.status(updated.status).json({ response: updated.response }); return res.status(updated.status).json({ response: updated.response });
} else if (req.method === "DELETE") { } else if (req.method === "DELETE") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const updated = await deleteUserById(userId, req.body, isServerAdmin); const updated = await deleteUserById(userId, req.body, isServerAdmin);
return res.status(updated.status).json({ response: updated.response }); return res.status(updated.status).json({ response: updated.response });
} }

View File

@ -5,6 +5,12 @@ import verifyUser from "@/lib/api/verifyUser";
export default async function users(req: NextApiRequest, res: NextApiResponse) { export default async function users(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") { if (req.method === "POST") {
if (process.env.DEMO_MODE === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const response = await postUser(req, res); const response = await postUser(req, res);
return res.status(response.status).json({ response: response.response }); return res.status(response.status).json({ response: response.response });
} else if (req.method === "GET") { } else if (req.method === "GET") {

View File

@ -128,7 +128,7 @@ export default function Index() {
style={{ color: activeCollection?.color }} style={{ color: activeCollection?.color }}
></i> ></i>
<p className="sm:text-4xl text-3xl capitalize w-full py-1 break-words hyphens-auto font-thin"> <p className="sm:text-3xl text-2xl capitalize w-full py-1 break-words hyphens-auto font-thin">
{activeCollection?.name} {activeCollection?.name}
</p> </p>
</div> </div>

View File

@ -145,7 +145,7 @@ export default function Index() {
<input <input
type="text" type="text"
autoFocus autoFocus
className="sm:text-4xl text-3xl capitalize bg-transparent h-10 w-3/4 outline-none border-b border-b-neutral-content" className="sm:text-3xl text-2xl capitalize bg-transparent h-10 w-3/4 outline-none border-b border-b-neutral-content"
value={newTagName} value={newTagName}
onChange={(e) => setNewTagName(e.target.value)} onChange={(e) => setNewTagName(e.target.value)}
/> />
@ -167,7 +167,7 @@ export default function Index() {
</> </>
) : ( ) : (
<> <>
<p className="sm:text-4xl text-3xl capitalize"> <p className="sm:text-3xl text-2xl capitalize">
{activeTag?.name} {activeTag?.name}
</p> </p>
<div className="relative"> <div className="relative">