From a1f48bbd79f81ff47bb5c6e4cc4c542a6ac9cdbf Mon Sep 17 00:00:00 2001 From: Oliver Schwamb Date: Fri, 5 Jul 2024 10:40:40 +0200 Subject: [PATCH 001/113] Tags in public collection --- .../EditCollectionSharingModal.tsx | 26 ++++++++ .../collectionId/updateCollectionById.ts | 1 + pages/public/collections/[id].tsx | 64 ++++++++++++++++++- prisma/schema.prisma | 1 + 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx index 9a73d5b..d4d587b 100644 --- a/components/ModalContent/EditCollectionSharingModal.tsx +++ b/components/ModalContent/EditCollectionSharingModal.tsx @@ -125,6 +125,32 @@ export default function EditCollectionSharingModal({ )} + {permissions === true && collection.isPublic && ( +
+

Show tags in public collection

+ + + +

+ This will let Anyone view this collections tags and search + this collections links by them. +

+
+ )} + {collection.isPublic ? (

Sharable Link (Click to copy)

diff --git a/lib/api/controllers/collections/collectionId/updateCollectionById.ts b/lib/api/controllers/collections/collectionId/updateCollectionById.ts index 4bb4d2f..0985c7b 100644 --- a/lib/api/controllers/collections/collectionId/updateCollectionById.ts +++ b/lib/api/controllers/collections/collectionId/updateCollectionById.ts @@ -62,6 +62,7 @@ export default async function updateCollection( description: data.description, color: data.color, isPublic: data.isPublic, + tagsArePublic: data.tagsArePublic, parent: data.parentId && data.parentId !== ("root" as any) ? { diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index d918086..7601856 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -3,6 +3,7 @@ import getPublicCollectionData from "@/lib/client/getPublicCollectionData"; import { CollectionIncludingMembersAndLinkCount, Sort, + TagIncludingLinkCount, ViewMode, } from "@/types/global"; import { useRouter } from "next/router"; @@ -24,6 +25,7 @@ import EditCollectionSharingModal from "@/components/ModalContent/EditCollection import ViewDropdown from "@/components/ViewDropdown"; import CardView from "@/components/LinkViews/Layouts/CardView"; import ListView from "@/components/LinkViews/Layouts/ListView"; +import useTagStore from "@/store/tags"; // import GridView from "@/components/LinkViews/Layouts/GridView"; const cardVariants: Variants = { @@ -56,6 +58,24 @@ export default function PublicCollections() { archiveAsPDF: undefined as unknown as boolean, }); + const { tags } = useTagStore(); + const handleTagSelection = (tag: TagIncludingLinkCount | 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.name || "") + ); + } else { + return router.push( + "/public/collections/" + + router.query.id) + } + } + const [searchFilter, setSearchFilter] = useState({ name: true, url: true, @@ -221,7 +241,49 @@ export default function PublicCollections() {
- +{collection.tagsArePublic && tags[0] && ( +
+

Browse by topic

+
+ + {tags + .sort((a, b) => a.name.localeCompare(b.name)) + .map((e, i) => { + const active = router.query.q === e.name; + return ( + + ); + }) + } +
+
+ )} {links[0] ? ( Date: Fri, 5 Jul 2024 10:50:32 +0200 Subject: [PATCH 002/113] Only show tags within collection --- pages/public/collections/[id].tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 7601856..a75f47f 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -44,6 +44,7 @@ const cardVariants: Variants = { export default function PublicCollections() { const { links } = useLinkStore(); + const tagsInCollection = links.map(l => l.tags).flat(); const { settings } = useLocalSettingsStore(); @@ -241,7 +242,7 @@ export default function PublicCollections() { -{collection.tagsArePublic && tags[0] && ( +{collection.tagsArePublic && tagsInCollection[0] && (

Browse by topic

@@ -257,7 +258,7 @@ export default function PublicCollections() {
- {tags + {tagsInCollection .sort((a, b) => a.name.localeCompare(b.name)) .map((e, i) => { const active = router.query.q === e.name; @@ -274,7 +275,7 @@ export default function PublicCollections() {

{e.name}

- {e._count?.links} + {tags.find(t => t.id === e.id)?._count?.links}
From e8d0cce58adcc65a1f987e4d7faab18c6fa7b97f Mon Sep 17 00:00:00 2001 From: Oliver Schwamb Date: Tue, 9 Jul 2024 13:50:08 +0200 Subject: [PATCH 003/113] Added `allLinksOfCollection` to linksStore Removed duplicated tags Fixed overflow for line added disclosure for tags in public collection --- hooks/useLinks.tsx | 65 +++++++++++++++++++------------ pages/public/collections/[id].tsx | 54 +++++++++++++++++++------ store/links.ts | 4 ++ 3 files changed, 87 insertions(+), 36 deletions(-) diff --git a/hooks/useLinks.tsx b/hooks/useLinks.tsx index bec2d49..6d0e935 100644 --- a/hooks/useLinks.tsx +++ b/hooks/useLinks.tsx @@ -18,7 +18,7 @@ export default function useLinks( searchByTextContent, }: LinkRequestQuery = { sort: 0 } ) { - const { links, setLinks, resetLinks, selectedLinks, setSelectedLinks } = + const { links, setLinks, resetLinks, selectedLinks, setSelectedLinks, setAllLinksOfCollection } = useLinkStore(); const router = useRouter(); @@ -26,6 +26,34 @@ export default function useLinks( const { reachedBottom, setReachedBottom } = useDetectPageBottom(); + const getPath = (params?: LinkRequestQuery) => { + const buildQueryString = (params: LinkRequestQuery) => { + return Object.keys(params) + .filter((key) => params[key as keyof LinkRequestQuery] !== undefined) + .map( + (key) => + `${encodeURIComponent(key)}=${encodeURIComponent( + params[key as keyof LinkRequestQuery] as string + )}` + ) + .join("&"); + }; + let queryString = ''; + if (params) { + queryString = buildQueryString(params); + } + + let basePath; + + if (router.pathname === "/dashboard") basePath = "/api/v1/dashboard"; + else if (router.pathname.startsWith("/public/collections/[id]")) { + queryString = queryString + "&collectionId=" + router.query.id; + basePath = "/api/v1/public/collections/links"; + } else basePath = "/api/v1/links"; + + return `${basePath}?${queryString}`; + } + const getLinks = async (isInitialCall: boolean, cursor?: number) => { const params = { sort, @@ -40,32 +68,10 @@ export default function useLinks( searchByTags, searchByTextContent, }; - - const buildQueryString = (params: LinkRequestQuery) => { - return Object.keys(params) - .filter((key) => params[key as keyof LinkRequestQuery] !== undefined) - .map( - (key) => - `${encodeURIComponent(key)}=${encodeURIComponent( - params[key as keyof LinkRequestQuery] as string - )}` - ) - .join("&"); - }; - - let queryString = buildQueryString(params); - - let basePath; - - if (router.pathname === "/dashboard") basePath = "/api/v1/dashboard"; - else if (router.pathname.startsWith("/public/collections/[id]")) { - queryString = queryString + "&collectionId=" + router.query.id; - basePath = "/api/v1/public/collections/links"; - } else basePath = "/api/v1/links"; - + setIsLoading(true); - const response = await fetch(`${basePath}?${queryString}`); + const response = await fetch(getPath(params)); const data = await response.json(); @@ -74,6 +80,14 @@ export default function useLinks( if (response.ok) setLinks(data.response, isInitialCall); }; + const getAllLinks = async () => { + const response = await fetch(getPath()); + + const data = await response.json(); + + if (response.ok) setAllLinksOfCollection(data.response); + }; + useEffect(() => { // Save the selected links before resetting the links // and then restore the selected links after resetting the links @@ -81,6 +95,7 @@ export default function useLinks( resetLinks(); setSelectedLinks(previouslySelected); + getAllLinks(); getLinks(true); }, [ router, diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index a75f47f..18c69dc 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -26,6 +26,7 @@ import ViewDropdown from "@/components/ViewDropdown"; import CardView from "@/components/LinkViews/Layouts/CardView"; import ListView from "@/components/LinkViews/Layouts/ListView"; import useTagStore from "@/store/tags"; +import { Disclosure, Transition } from "@headlessui/react"; // import GridView from "@/components/LinkViews/Layouts/GridView"; const cardVariants: Variants = { @@ -43,9 +44,11 @@ const cardVariants: Variants = { }; export default function PublicCollections() { - const { links } = useLinkStore(); - const tagsInCollection = links.map(l => l.tags).flat(); - + const { links, allLinksOfCollection } = useLinkStore(); + const tagsInCollection = allLinksOfCollection.map(l => l.tags).flat().filter((value, index, self) => + index === self.findIndex((t) => ( + t.name === value.name + ))); const { settings } = useLocalSettingsStore(); const router = useRouter(); @@ -101,7 +104,6 @@ export default function PublicCollections() { const [collection, setCollection] = useState(); - useEffect(() => { if (router.query.id) { getPublicCollectionData(Number(router.query.id), setCollection); @@ -132,6 +134,14 @@ export default function PublicCollections() { [ViewMode.List]: ListView, }; + const [tagDisclosure, setTagDisclosure] = useState(() => { + const storedValue = localStorage.getItem("tagDisclosureForPublicCollection" + collection?.id); + return storedValue ? storedValue === "true" : true; + }); + useEffect(() => { + localStorage.setItem("tagDisclosureForPublicCollection" + collection?.id, tagDisclosure ? "true" : "false"); + }, [tagDisclosure]); + // @ts-ignore const LinkComponent = linkView[viewMode]; @@ -243,10 +253,31 @@ export default function PublicCollections() { {collection.tagsArePublic && tagsInCollection[0] && ( -
-

Browse by topic

-
-
-{collection.tagsArePublic && tagsInCollection[0] && ( +{collection.tagsArePublic && tags[0] && ( { @@ -289,7 +286,7 @@ export default function PublicCollections() { - {tagsInCollection + {tags .sort((a, b) => a.name.localeCompare(b.name)) .map((e, i) => { const active = router.query.q === e.name; @@ -306,7 +303,7 @@ export default function PublicCollections() {

{e.name}

- {tags.find(t => t.id === e.id)?._count?.links} + {e._count?.links}
diff --git a/store/links.ts b/store/links.ts index 8391f55..408a3ee 100644 --- a/store/links.ts +++ b/store/links.ts @@ -11,13 +11,11 @@ type ResponseObject = { type LinkStore = { links: LinkIncludingShortenedCollectionAndTags[]; selectedLinks: LinkIncludingShortenedCollectionAndTags[]; - allLinksOfCollection: LinkIncludingShortenedCollectionAndTags[]; setLinks: ( data: LinkIncludingShortenedCollectionAndTags[], isInitialCall: boolean ) => void; setSelectedLinks: (links: LinkIncludingShortenedCollectionAndTags[]) => void; - setAllLinksOfCollection: (links: LinkIncludingShortenedCollectionAndTags[]) => void; addLink: ( body: LinkIncludingShortenedCollectionAndTags ) => Promise; @@ -41,7 +39,6 @@ type LinkStore = { const useLinkStore = create()((set) => ({ links: [], selectedLinks: [], - allLinksOfCollection: [], setLinks: async (data, isInitialCall) => { isInitialCall && set(() => ({ @@ -61,7 +58,6 @@ const useLinkStore = create()((set) => ({ })); }, setSelectedLinks: (links) => set({ selectedLinks: links }), - setAllLinksOfCollection: (links) => set({allLinksOfCollection: links}), addLink: async (body) => { const response = await fetch("/api/v1/links", { body: JSON.stringify(body), diff --git a/store/tags.ts b/store/tags.ts index 6778152..f8a0337 100644 --- a/store/tags.ts +++ b/store/tags.ts @@ -8,15 +8,19 @@ type ResponseObject = { type TagStore = { tags: TagIncludingLinkCount[]; - setTags: () => void; + setTags: (collectionId?: number) => void; updateTag: (tag: TagIncludingLinkCount) => Promise; removeTag: (tagId: number) => Promise; }; const useTagStore = create()((set) => ({ tags: [], - setTags: async () => { - const response = await fetch("/api/v1/tags"); + setTags: async (collectionId?: number) => { + let path = "/api/v1/tags"; + if (collectionId) { + path = "/api/v1/public/collections/tags?collectionId=" + encodeURIComponent(collectionId); + } + const response = await fetch(path); const data = await response.json(); From e79b98d3b000eda1abc6eec18fe0a4348f6df357 Mon Sep 17 00:00:00 2001 From: Isaac Wise Date: Mon, 22 Jul 2024 22:34:36 -0500 Subject: [PATCH 005/113] Replace useless ternarys with logical ANDs --- components/CollectionCard.tsx | 28 +++++------ components/CollectionListing.tsx | 9 ++-- .../LinkViews/LinkComponents/LinkActions.tsx | 34 ++++++------- components/LinkViews/LinkList.tsx | 13 +++-- components/MobileNavigation.tsx | 19 ++++--- components/ModalContent/EditLinkModal.tsx | 4 +- components/ModalContent/NewLinkModal.tsx | 4 +- components/ModalContent/NewUserModal.tsx | 4 +- .../ModalContent/PreservedFormatsModal.tsx | 31 ++++++------ components/ModalContent/UploadFileModal.tsx | 8 +-- components/Navbar.tsx | 12 ++--- components/NoLinksFound.tsx | 4 +- components/PreserverdFormatRow.tsx | 9 ++-- components/ReadableView.tsx | 21 ++++---- layouts/CenteredForm.tsx | 13 +++-- layouts/MainLayout.tsx | 4 +- pages/collections/[id].tsx | 43 ++++++++-------- pages/collections/index.tsx | 8 +-- pages/dashboard.tsx | 4 +- pages/login.tsx | 10 ++-- pages/public/collections/[id].tsx | 49 +++++++++---------- pages/register.tsx | 26 +++++----- pages/settings/access-tokens.tsx | 8 +-- pages/settings/account.tsx | 38 +++++++------- pages/settings/delete.tsx | 4 +- 25 files changed, 195 insertions(+), 212 deletions(-) diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index da634cc..cd7d80e 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -129,12 +129,12 @@ export default function CollectionCard({ collection, className }: Props) { className="flex items-center absolute bottom-3 left-3 z-10 btn px-2 btn-ghost rounded-full" onClick={() => setEditCollectionSharingModal(true)} > - {collectionOwner.id ? ( + {collectionOwner.id && ( - ) : undefined} + )} {collection.members .sort((a, b) => (a.userId as number) - (b.userId as number)) .map((e, i) => { @@ -159,11 +159,9 @@ export default function CollectionCard({ collection, className }: Props) { @@ -178,12 +176,12 @@ export default function CollectionCard({ collection, className }: Props) {
- {collection.isPublic ? ( + {collection.isPublic && ( - ) : undefined} + )}
- {editCollectionModal ? ( + {editCollectionModal && ( setEditCollectionModal(false)} activeCollection={collection} /> - ) : undefined} - {editCollectionSharingModal ? ( + )} + {editCollectionSharingModal && ( setEditCollectionSharingModal(false)} activeCollection={collection} /> - ) : undefined} - {deleteCollectionModal ? ( + )} + {deleteCollectionModal && ( setDeleteCollectionModal(false)} activeCollection={collection} /> - ) : undefined} + )}
); } diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index fbe2f7b..b26e8cf 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -231,11 +231,10 @@ const renderItem = ( return (
{Icon(item as ExtendedTreeItem, onExpand, onCollapse)} @@ -253,12 +252,12 @@ const renderItem = ( >

{collection.name}

- {collection.isPublic ? ( + {collection.isPublic && ( - ) : undefined} + )}
{collection._count?.links}
diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index c68dd06..41f40c5 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -79,9 +79,8 @@ export default function LinkActions({ return ( <>
  • - {linkInfo !== undefined && toggleShowInfo ? ( + {linkInfo !== undefined && toggleShowInfo && (
  • - ) : undefined} - {permissions === true || permissions?.canUpdate ? ( + )} + {permissions === true || permissions?.canUpdate && (
  • - ) : undefined} + )} {link.type === "url" && (
  • )} - {permissions === true || permissions?.canDelete ? ( + {permissions === true || permissions?.canDelete && (
  • - ) : undefined} + )}
- {editLinkModal ? ( + {editLinkModal && ( setEditLinkModal(false)} activeLink={link} /> - ) : undefined} - {deleteLinkModal ? ( + )} + {deleteLinkModal && ( setDeleteLinkModal(false)} activeLink={link} /> - ) : undefined} - {preservedFormatsModal ? ( + )} + {preservedFormatsModal && ( setPreservedFormatsModal(false)} activeLink={link} /> - ) : undefined} + )} {/* {expandedLink ? ( setExpandedLink(false)} link={link} /> ) : undefined} */} diff --git a/components/LinkViews/LinkList.tsx b/components/LinkViews/LinkList.tsx index 723c47e..efb10c4 100644 --- a/components/LinkViews/LinkList.tsx +++ b/components/LinkViews/LinkList.tsx @@ -91,9 +91,8 @@ export default function LinkCardCompact({ return ( <>
selectable ? handleCheckboxClick(link) @@ -139,9 +138,9 @@ export default function LinkCardCompact({
- {collection ? ( + {collection && ( - ) : undefined} + )} {link.name && }
@@ -153,8 +152,8 @@ export default function LinkCardCompact({ collection={collection} position="top-3 right-3" flipDropdown={flipDropdown} - // toggleShowInfo={() => setShowInfo(!showInfo)} - // linkInfo={showInfo} + // toggleShowInfo={() => setShowInfo(!showInfo)} + // linkInfo={showInfo} />
- {newLinkModal ? ( + {newLinkModal && ( setNewLinkModal(false)} /> - ) : undefined} - {newCollectionModal ? ( + )} + {newCollectionModal && ( setNewCollectionModal(false)} /> - ) : undefined} - {uploadFileModal ? ( + )} + {uploadFileModal && ( setUploadFileModal(false)} /> - ) : undefined} + )} ); } diff --git a/components/ModalContent/EditLinkModal.tsx b/components/ModalContent/EditLinkModal.tsx index 1d873c7..b509aa8 100644 --- a/components/ModalContent/EditLinkModal.tsx +++ b/components/ModalContent/EditLinkModal.tsx @@ -72,7 +72,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
- {link.url ? ( + {link.url && (

{shortenedURL}

- ) : undefined} + )}

{t("name")}

diff --git a/components/ModalContent/NewLinkModal.tsx b/components/ModalContent/NewLinkModal.tsx index 0284687..680f9ef 100644 --- a/components/ModalContent/NewLinkModal.tsx +++ b/components/ModalContent/NewLinkModal.tsx @@ -128,7 +128,7 @@ export default function NewLinkModal({ onClose }: Props) {
- {optionsExpanded ? ( + {optionsExpanded && (
@@ -163,7 +163,7 @@ export default function NewLinkModal({ onClose }: Props) {
- ) : undefined} + )}
- {emailEnabled ? ( + {emailEnabled && (

{t("email")}

- ) : undefined} + )}

diff --git a/components/ModalContent/PreservedFormatsModal.tsx b/components/ModalContent/PreservedFormatsModal.tsx index 87e84a7..4414b37 100644 --- a/components/ModalContent/PreservedFormatsModal.tsx +++ b/components/ModalContent/PreservedFormatsModal.tsx @@ -150,16 +150,16 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {

{t("preserved_formats")}

{screenshotAvailable(link) || - pdfAvailable(link) || - readabilityAvailable(link) || - monolithAvailable(link) ? ( + pdfAvailable(link) || + readabilityAvailable(link) || + monolithAvailable(link) ? (

{t("available_formats")}

) : ( "" )}
- {monolithAvailable(link) ? ( + {monolithAvailable(link) && ( - ) : undefined} + )} - {screenshotAvailable(link) ? ( + {screenshotAvailable(link) && ( - ) : undefined} + )} - {pdfAvailable(link) ? ( + {pdfAvailable(link) && ( - ) : undefined} + )} - {readabilityAvailable(link) ? ( + {readabilityAvailable(link) && ( - ) : undefined} + )} {!isReady() && !atLeastOneFormatAvailable() ? (
@@ -213,7 +213,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {

{t("preservation_in_queue")}

{t("check_back_later")}

- ) : !isReady() && atLeastOneFormatAvailable() ? ( + ) : !isReady() && atLeastOneFormatAvailable() && (
{t("there_are_more_formats")}

{t("check_back_later")}

- ) : undefined} + )}

{t("collection")}

- {link.collection.name ? ( + {link.collection.name && ( - ) : null} + )}
- {optionsExpanded ? ( + {optionsExpanded && (
@@ -203,7 +203,7 @@ export default function UploadFileModal({ onClose }: Props) {
- ) : undefined} + )}
setOptionsExpanded(!optionsExpanded)} diff --git a/components/Navbar.tsx b/components/Navbar.tsx index dcc78c5..888c56b 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -120,15 +120,15 @@ export default function Navbar() {
) : null} - {newLinkModal ? ( + {newLinkModal && ( setNewLinkModal(false)} /> - ) : undefined} - {newCollectionModal ? ( + )} + {newCollectionModal && ( setNewCollectionModal(false)} /> - ) : undefined} - {uploadFileModal ? ( + )} + {uploadFileModal && ( setUploadFileModal(false)} /> - ) : undefined} + )}
); } diff --git a/components/NoLinksFound.tsx b/components/NoLinksFound.tsx index 516dfb6..b42516a 100644 --- a/components/NoLinksFound.tsx +++ b/components/NoLinksFound.tsx @@ -39,9 +39,9 @@ export default function NoLinksFound({ text }: Props) {
- {newLinkModal ? ( + {newLinkModal && ( setNewLinkModal(false)} /> - ) : undefined} + )}
); } diff --git a/components/PreserverdFormatRow.tsx b/components/PreserverdFormatRow.tsx index ce0f21d..e28d43d 100644 --- a/components/PreserverdFormatRow.tsx +++ b/components/PreserverdFormatRow.tsx @@ -97,19 +97,18 @@ export default function PreservedFormatRow({
- {downloadable || false ? ( + {downloadable || false && (
handleDownload()} className="btn btn-sm btn-square" >
- ) : undefined} + )} diff --git a/components/ReadableView.tsx b/components/ReadableView.tsx index b600236..b862b38 100644 --- a/components/ReadableView.tsx +++ b/components/ReadableView.tsx @@ -183,7 +183,7 @@ export default function ReadableView({ link }: Props) { link?.name || link?.description || link?.url || "" )}

- {link?.url ? ( + {link?.url && ( - {isValidUrl(link?.url || "") - ? new URL(link?.url as string).host - : undefined} + {isValidUrl(link?.url || "") && new URL(link?.url as string).host} - ) : undefined} + )}
@@ -231,10 +229,10 @@ export default function ReadableView({ link }: Props) {

{date ? new Date(date).toLocaleString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - }) + year: "numeric", + month: "long", + day: "numeric", + }) : undefined}

@@ -259,9 +257,8 @@ export default function ReadableView({ link }: Props) { >
) : (
- {settings.theme ? ( + {settings.theme && ( Linkwarden - ) : undefined} - {text ? ( + )} + {text && (

{text}

- ) : undefined} + )} {children}

- {showAnnouncement ? ( + {showAnnouncement && ( - ) : undefined} + )}

diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index bd24a95..b50d6df 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -115,9 +115,8 @@ export default function Index() {
{activeCollection && ( @@ -211,12 +210,12 @@ export default function Index() { className="flex items-center btn px-2 btn-ghost rounded-full w-fit" onClick={() => setEditCollectionSharingModal(true)} > - {collectionOwner.id ? ( + {collectionOwner.id && ( - ) : undefined} + )} {activeCollection.members .sort((a, b) => (a.userId as number) - (b.userId as number)) .map((e, i) => { @@ -241,20 +240,20 @@ export default function Index() {

{activeCollection.members.length > 0 && - activeCollection.members.length === 1 + activeCollection.members.length === 1 ? t("by_author_and_other", { + author: collectionOwner.name, + count: activeCollection.members.length, + }) + : activeCollection.members.length > 0 && + activeCollection.members.length !== 1 + ? t("by_author_and_others", { author: collectionOwner.name, count: activeCollection.members.length, }) - : activeCollection.members.length > 0 && - activeCollection.members.length !== 1 - ? t("by_author_and_others", { - author: collectionOwner.name, - count: activeCollection.members.length, - }) : t("by_author", { - author: collectionOwner.name, - })} + author: collectionOwner.name, + })}

@@ -299,15 +298,15 @@ export default function Index() { setSortBy={setSortBy} editMode={ permissions === true || - permissions?.canUpdate || - permissions?.canDelete + permissions?.canUpdate || + permissions?.canDelete ? editMode : undefined } setEditMode={ permissions === true || - permissions?.canUpdate || - permissions?.canDelete + permissions?.canUpdate || + permissions?.canDelete ? setEditMode : undefined } @@ -315,11 +314,11 @@ export default function Index() {

{activeCollection?._count?.links === 1 ? t("showing_count_result", { - count: activeCollection?._count?.links, - }) + count: activeCollection?._count?.links, + }) : t("showing_count_results", { - count: activeCollection?._count?.links, - })} + count: activeCollection?._count?.links, + })}

diff --git a/pages/collections/index.tsx b/pages/collections/index.tsx index 642e3dd..717732f 100644 --- a/pages/collections/index.tsx +++ b/pages/collections/index.tsx @@ -58,7 +58,7 @@ export default function Collections() {
- {sortedCollections.filter((e) => e.ownerId !== data?.user.id)[0] ? ( + {sortedCollections.filter((e) => e.ownerId !== data?.user.id)[0] && ( <> - ) : undefined} + )} - {newCollectionModal ? ( + {newCollectionModal && ( setNewCollectionModal(false)} /> - ) : undefined} + )} ); } diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index 3d1fdb6..78239de 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -324,9 +324,9 @@ export default function Dashboard() { )} - {newLinkModal ? ( + {newLinkModal && ( setNewLinkModal(false)} /> - ) : undefined} + )} ); } diff --git a/pages/login.tsx b/pages/login.tsx index c1bc8d8..4eb59c8 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -203,9 +203,9 @@ export default function Login({ {t("login")} - {availableLogins.buttonAuths.length > 0 ? ( + {availableLogins.buttonAuths.length > 0 && (
{t("or_continue_with")}
- ) : undefined} + )} ); } @@ -224,9 +224,9 @@ export default function Login({ loading={submitLoader} > {value.name.toLowerCase() === "google" || - value.name.toLowerCase() === "apple" ? ( - - ) : undefined} + value.name.toLowerCase() === "apple" && ( + + )} {value.name} diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 47e074f..454a957 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -104,16 +104,17 @@ export default function PublicCollections() { // @ts-ignore const LinkComponent = linkView[viewMode]; - return collection ? ( + if (!collection) return null; + + return (
- {collection ? ( + {collection && ( {collection.name} | Linkwarden - ) : undefined} + )}

@@ -151,12 +152,12 @@ export default function PublicCollections() { className="flex items-center btn px-2 btn-ghost rounded-full" onClick={() => setEditCollectionSharingModal(true)} > - {collectionOwner.id ? ( + {collectionOwner.id && ( - ) : undefined} + )} {collection.members .sort((a, b) => (a.userId as number) - (b.userId as number)) .map((e, i) => { @@ -181,20 +182,20 @@ export default function PublicCollections() {

{collection.members.length > 0 && - collection.members.length === 1 + collection.members.length === 1 ? t("by_author_and_other", { + author: collectionOwner.name, + count: collection.members.length, + }) + : collection.members.length > 0 && + collection.members.length !== 1 + ? t("by_author_and_others", { author: collectionOwner.name, count: collection.members.length, }) - : 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, - })} + author: collectionOwner.name, + })}

@@ -218,11 +219,11 @@ export default function PublicCollections() { placeholder={ collection._count?.links === 1 ? t("search_count_link", { - count: collection._count?.links, - }) + count: collection._count?.links, + }) : t("search_count_links", { - count: collection._count?.links, - }) + count: collection._count?.links, + }) } /> @@ -248,15 +249,13 @@ export default function PublicCollections() {

*/}
- {editCollectionSharingModal ? ( + {editCollectionSharingModal && ( setEditCollectionSharingModal(false)} activeCollection={collection} /> - ) : undefined} + )} - ) : ( - <> ); } diff --git a/pages/register.tsx b/pages/register.tsx index 4eba2bc..6e7fc1e 100644 --- a/pages/register.tsx +++ b/pages/register.tsx @@ -133,9 +133,9 @@ export default function Register({ loading={submitLoader} > {value.name.toLowerCase() === "google" || - value.name.toLowerCase() === "apple" ? ( - - ) : undefined} + value.name.toLowerCase() === "apple" && ( + + )} {value.name} @@ -149,8 +149,8 @@ export default function Register({ text={ process.env.NEXT_PUBLIC_STRIPE ? t("trial_offer_desc", { - count: Number(process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14), - }) + count: Number(process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14), + }) : t("register_desc") } data-testid="registration-form" @@ -201,7 +201,7 @@ export default function Register({ )} - {emailEnabled ? ( + {emailEnabled && (

{t("email")}

@@ -214,7 +214,7 @@ export default function Register({ onChange={(e) => setForm({ ...form, email: e.target.value })} />
- ) : undefined} + )}

@@ -248,7 +248,7 @@ export default function Register({ />

- {process.env.NEXT_PUBLIC_STRIPE ? ( + {process.env.NEXT_PUBLIC_STRIPE && (

- ) : undefined} + )} - {tokens.length > 0 ? ( + {tokens.length > 0 && ( @@ -93,12 +93,12 @@ export default function AccessTokens() { ))}
- ) : undefined} + )} - {newTokenModal ? ( + {newTokenModal && ( setNewTokenModal(false)} /> - ) : undefined} + )} {revokeTokenModal && selectedToken && ( { diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index cb46442..2ba80fa 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -29,19 +29,19 @@ export default function Account() { !objectIsEmpty(account) ? account : ({ - // @ts-ignore - id: null, - name: "", - username: "", - email: "", - emailVerified: null, - password: undefined, - image: "", - isPrivate: true, - // @ts-ignore - createdAt: null, - whitelistedUsers: [], - } as unknown as AccountSettings) + // @ts-ignore + id: null, + name: "", + username: "", + email: "", + emailVerified: null, + password: undefined, + image: "", + isPrivate: true, + // @ts-ignore + createdAt: null, + whitelistedUsers: [], + } as unknown as AccountSettings) ); const { t } = useTranslation(); @@ -176,7 +176,7 @@ export default function Account() { onChange={(e) => setUser({ ...user, username: e.target.value })} /> - {emailEnabled ? ( + {emailEnabled && (

{t("email")}

setUser({ ...user, email: e.target.value })} />
- ) : undefined} + )}

{t("language")}

e.target.files && setFile(e.target.files[0])} /> diff --git a/components/ToggleDarkMode.tsx b/components/ToggleDarkMode.tsx index 9404d41..28631b9 100644 --- a/components/ToggleDarkMode.tsx +++ b/components/ToggleDarkMode.tsx @@ -21,9 +21,8 @@ export default function ToggleDarkMode({ className }: Props) { useEffect(() => { if (theme) { updateSettings({ theme }); - localStorage.setItem("theme", theme); } - }, [theme, updateSettings]); + }, [theme]); return (
redactIds(item)); - } else if (data !== null && typeof data === "object") { - const fieldsToRedact = ["id", "parentId", "collectionId", "ownerId"]; - - fieldsToRedact.forEach((field) => { - if (field in data) { - delete (data as any)[field]; - } - }); - - // Recursively call redactIds for each property that is an object or an array - Object.keys(data).forEach((key) => { - const value = (data as any)[key]; - if ( - value !== null && - (typeof value === "object" || Array.isArray(value)) - ) { - redactIds(value); - } - }); - } - } - - redactIds(userData); - return { response: userData, status: 200 }; } diff --git a/lib/api/generatePreview.ts b/lib/api/generatePreview.ts index 2dc325b..1c6ca94 100644 --- a/lib/api/generatePreview.ts +++ b/lib/api/generatePreview.ts @@ -16,7 +16,7 @@ const generatePreview = async ( return; } - image.resize(1280, Jimp.AUTO).quality(20); + image.resize(1000, Jimp.AUTO).quality(20); const processedBuffer = await image.getBufferAsync(Jimp.MIME_JPEG); if ( diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index e0b107f..0bdb525 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -135,7 +135,7 @@ export default function Dashboard() {
diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 4c5f777..d49660f 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -88,160 +88,162 @@ export default function PublicCollections() { (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - if (!collection) return null; + if (!collection) return <>; + else + return ( +
+ {collection && ( + + {collection.name} | Linkwarden + + + )} +
+
+

+ {collection.name} +

+
+ - return ( -
- {collection && ( - - {collection.name} | Linkwarden - - - )} -
-
-

- {collection.name} -

-
- - - - Linkwarden - + + Linkwarden + +
-
-
-
-
-
setEditCollectionSharingModal(true)} - > - {collectionOwner.id && ( - - )} - {collection.members - .sort((a, b) => (a.userId as number) - (b.userId as number)) - .map((e, i) => { - return ( - - ); - }) - .slice(0, 3)} - {collection.members.length - 3 > 0 && ( -
-
- +{collection.members.length - 3} -
-
- )} -
- -

- {collection.members.length > 0 && - collection.members.length === 1 - ? t("by_author_and_other", { - author: collectionOwner.name, - count: collection.members.length, +

+
+
+
setEditCollectionSharingModal(true)} + > + {collectionOwner.id && ( + + )} + {collection.members + .sort((a, b) => (a.userId as number) - (b.userId as number)) + .map((e, i) => { + return ( + + ); }) - : collection.members.length > 0 && - collection.members.length !== 1 - ? t("by_author_and_others", { + .slice(0, 3)} + {collection.members.length - 3 > 0 && ( +
+
+ +{collection.members.length - 3} +
+
+ )} +
+ +

+ {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, - })} -

+ : 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, + })} +

+
-
-

{collection.description}

+

{collection.description}

-
+
-
- - + + + + + { + const linkWithCollectionData = { + ...e, + collection: collection, // Append collection data + }; + return linkWithCollectionData; + }) as any } + layout={viewMode} + placeholderCount={1} + useData={data} /> - + {!data.isLoading && links && !links[0] && ( +

{t("nothing_found")}

+ )} - { - const linkWithCollectionData = { - ...e, - collection: collection, // Append collection data - }; - return linkWithCollectionData; - }) as any - } - layout={viewMode} - placeholderCount={1} - useData={data} - /> - {!data.isLoading && links && !links[0] &&

{t("nothing_found")}

} - - {/*

+ {/*

List created with Linkwarden.

*/} +
+ {editCollectionSharingModal && ( + setEditCollectionSharingModal(false)} + activeCollection={collection} + /> + )}
- {editCollectionSharingModal && ( - setEditCollectionSharingModal(false)} - activeCollection={collection} - /> - )} -
- ); + ); } export { getServerSideProps }; From 2893d3caf22b7e4188cd41b2217c33a9c6d39197 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Sun, 18 Aug 2024 16:52:08 -0400 Subject: [PATCH 026/113] minor improvement --- pages/links/[id].tsx | 10 ++++++---- pages/public/links/[id].tsx | 12 +++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/pages/links/[id].tsx b/pages/links/[id].tsx index 9f2aaec..55f5648 100644 --- a/pages/links/[id].tsx +++ b/pages/links/[id].tsx @@ -19,10 +19,12 @@ const Index = () => { return (
{getLink.data ? ( - +
+ +
) : (
diff --git a/pages/public/links/[id].tsx b/pages/public/links/[id].tsx index 9f2aaec..8f3ef13 100644 --- a/pages/public/links/[id].tsx +++ b/pages/public/links/[id].tsx @@ -17,12 +17,14 @@ const Index = () => { }, []); return ( -
+
{getLink.data ? ( - +
+ +
) : (
From 1062e070654354a95517825bc9d18c646879667b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=A6=E7=94=B0=20=E6=B7=B3=E4=B8=80?= Date: Tue, 20 Aug 2024 00:50:07 +0900 Subject: [PATCH 027/113] Created Japanese Translate --- public/locales/jp/common.json | 375 ++++++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 public/locales/jp/common.json diff --git a/public/locales/jp/common.json b/public/locales/jp/common.json new file mode 100644 index 0000000..f744c1d --- /dev/null +++ b/public/locales/jp/common.json @@ -0,0 +1,375 @@ +{ + "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>利用規約 及び <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 までお問い合わせください。", + "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_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. 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>詳しくはこちら", + "full_content": "全コンテンツ", + "slower": "遅い", + "new_version_announcement": "新しいバージョンの <0>Linkwarden {{version}} をご覧ください!", + "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>注意: パスワードを変更する必要があることをユーザーに知らせてください。", + "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": "リセット", + "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": "リンクの詳細を表示", + "hide_link_details": "リンクの詳細を非表示", + "link_pinned": "リンクがピン留めされました!", + "link_unpinned": "リンクのピンが外れました!", + "webpage": "ウェブページ", + "server_administration": "サーバー管理", + "all_collections": "すべてのコレクション", + "dashboard": "ダッシュボード", + "demo_title": "デモ専用", + "demo_desc": "これはLinkwardenのデモインスタンスであり、アップロードは無効になっています。", + "demo_desc_2": "完全版を試したい場合は、以下で無料トライアルにサインアップできます:", + "demo_button": "デモユーザーとしてログイン" +} \ No newline at end of file From 21578bac8d3a6d2e15036d596b8ac4776c55a89d Mon Sep 17 00:00:00 2001 From: Dan Jacobsen Date: Mon, 19 Aug 2024 12:44:59 -0700 Subject: [PATCH 028/113] feat: add configurable max workers --- .env.sample | 1 + playwright.config.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index 22bda55..b02dc44 100644 --- a/.env.sample +++ b/.env.sample @@ -33,6 +33,7 @@ SCREENSHOT_MAX_BUFFER= READABILITY_MAX_BUFFER= PREVIEW_MAX_BUFFER= IMPORT_LIMIT= +MAX_WORKERS= # AWS S3 Settings SPACES_KEY= diff --git a/playwright.config.ts b/playwright.config.ts index b90f7d7..265a274 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 1 : Number(process.env.MAX_WORKERS) || undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ From dc388ebba550e9343f89965cd8dcdfcc102baecb Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Mon, 19 Aug 2024 18:14:09 -0400 Subject: [PATCH 029/113] improved iconPicker component + other improvements --- components/Icon.tsx | 16 +++ components/IconPicker.tsx | 104 ++++++++++++++---- .../LinkViews/LinkComponents/LinkActions.tsx | 40 +++---- .../ModalContent/EditCollectionModal.tsx | 40 +++++-- components/ModalContent/LinkDetailModal.tsx | 33 +++--- .../ModalContent/NewCollectionModal.tsx | 4 +- components/ModalContent/NewLinkModal.tsx | 3 + components/ModalContent/UploadFileModal.tsx | 3 + components/Popover.tsx | 21 ++++ components/ReadableView.tsx | 2 +- pages/collections/[id].tsx | 2 +- .../migration.sql | 8 ++ .../migration.sql | 3 + prisma/schema.prisma | 7 +- styles/globals.css | 8 ++ 15 files changed, 222 insertions(+), 72 deletions(-) create mode 100644 components/Icon.tsx create mode 100644 components/Popover.tsx create mode 100644 prisma/migrations/20240819003838_add_icon_related_fields_to_link_and_collection_model/migration.sql create mode 100644 prisma/migrations/20240819005010_remove_collection_model_default_color/migration.sql diff --git a/components/Icon.tsx b/components/Icon.tsx new file mode 100644 index 0000000..178b9e6 --- /dev/null +++ b/components/Icon.tsx @@ -0,0 +1,16 @@ +import React, { forwardRef } from "react"; +import * as Icons from "@phosphor-icons/react"; + +type Props = { + icon: string; +} & Icons.IconProps; + +const Icon = forwardRef(({ icon, ...rest }) => { + const IconComponent: any = Icons[icon as keyof typeof Icons]; + + if (!IconComponent) { + return <>; + } else return ; +}); + +export default Icon; diff --git a/components/IconPicker.tsx b/components/IconPicker.tsx index 994a5af..f90dc1f 100644 --- a/components/IconPicker.tsx +++ b/components/IconPicker.tsx @@ -2,6 +2,9 @@ import { icons } from "@/lib/client/icons"; import React, { useMemo, useState } from "react"; import Fuse from "fuse.js"; import TextInput from "./TextInput"; +import Popover from "./Popover"; +import { HexColorPicker } from "react-colorful"; +import Icon from "./Icon"; const fuse = new Fuse(icons, { keys: [{ name: "name", weight: 4 }, "tags", "categories"], @@ -9,9 +12,29 @@ const fuse = new Fuse(icons, { useExtendedSearch: true, }); -type Props = {}; +type Props = { + onClose: Function; + alignment?: "left" | "right" | "bottom" | "top"; + color: string; + setColor: Function; + iconName: string; + setIconName: Function; + weight: "light" | "regular" | "bold" | "fill" | "duotone"; + setWeight: Function; + className?: string; +}; -const IconPicker = (props: Props) => { +const IconPicker = ({ + onClose, + alignment, + color, + setColor, + iconName, + setIconName, + weight, + setWeight, + className, +}: Props) => { const [query, setQuery] = useState(""); const filteredQueryResultsSelector = useMemo(() => { @@ -22,24 +45,67 @@ const IconPicker = (props: Props) => { }, [query]); return ( -
- setQuery(e.target.value)} - /> -
- {filteredQueryResultsSelector.map((icon) => { - const IconComponent = icon.Icon; - return ( -
console.log(icon.name)}> - -
- ); - })} + +
+
+
+ + + +
+ setColor(e)} /> +
+ + setQuery(e.target.value)} + /> + +
+ {filteredQueryResultsSelector.map((icon) => { + const IconComponent = icon.Icon; + return ( +
setIconName(icon.pascal_name)} + className={`cursor-pointer btn p-1 box-border ${ + icon.pascal_name === iconName + ? "outline outline-1 outline-primary" + : "" + }`} + > + +
+ ); + })} +
-
+ ); }; diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index f93f969..0e04491 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -105,24 +105,23 @@ export default function LinkActions({ alignToTop ? "" : "translate-y-10" }`} > - {permissions === true || - (permissions?.canUpdate && ( -
  • -
    { - (document?.activeElement as HTMLElement)?.blur(); - pinLink(); - }} - className="whitespace-nowrap" - > - {link?.pinnedBy && link.pinnedBy[0] - ? t("unpin") - : t("pin_to_dashboard")} -
    -
  • - ))} + {(permissions === true || permissions?.canUpdate) && ( +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + pinLink(); + }} + className="whitespace-nowrap" + > + {link?.pinnedBy && link.pinnedBy[0] + ? t("unpin") + : t("pin_to_dashboard")} +
    +
  • + )}
  • { (document?.activeElement as HTMLElement)?.blur(); + console.log(e.shiftKey); e.shiftKey - ? async () => { + ? (async () => { const load = toast.loading(t("deleting")); await deleteLink.mutateAsync(link.id as number, { @@ -188,7 +188,7 @@ export default function LinkActions({ } }, }); - } + })() : setDeleteLinkModal(true); }} className="whitespace-nowrap" diff --git a/components/ModalContent/EditCollectionModal.tsx b/components/ModalContent/EditCollectionModal.tsx index f8dfd20..76833e7 100644 --- a/components/ModalContent/EditCollectionModal.tsx +++ b/components/ModalContent/EditCollectionModal.tsx @@ -6,6 +6,8 @@ import Modal from "../Modal"; import { useTranslation } from "next-i18next"; import { useUpdateCollection } from "@/hooks/store/collections"; import toast from "react-hot-toast"; +import IconPicker from "../IconPicker"; +import Icon from "../Icon"; type Props = { onClose: Function; @@ -20,6 +22,7 @@ export default function EditCollectionModal({ const [collection, setCollection] = useState(activeCollection); + const [iconPicker, setIconPicker] = useState(false); const [submitLoader, setSubmitLoader] = useState(false); const updateCollection = useUpdateCollection(); @@ -71,17 +74,32 @@ export default function EditCollectionModal({

    {t("color")}

    - - setCollection({ ...collection, color }) - } - /> -
    - +
    + setIconPicker(true)} + /> + {iconPicker && ( + setIconPicker(false)} + className="top-20" + color={collection.color as string} + setColor={(color: string) => + setCollection({ ...collection, color }) + } + weight={collection.iconWeight as any} + setWeight={(iconWeight: string) => + setCollection({ ...collection, iconWeight }) + } + iconName={collection.icon as string} + setIconName={(icon: string) => + setCollection({ ...collection, icon }) + } + /> + )}
    diff --git a/components/ModalContent/LinkDetailModal.tsx b/components/ModalContent/LinkDetailModal.tsx index e9f0086..ad7d36c 100644 --- a/components/ModalContent/LinkDetailModal.tsx +++ b/components/ModalContent/LinkDetailModal.tsx @@ -120,25 +120,24 @@ export default function LinkDetailModal({ onClose, onEdit, link }: Props) {
    - {permissions === true || - (permissions?.canUpdate && ( - <> -
    -
    + {(permissions === true || permissions?.canUpdate) && ( + <> +
    +
    -
    -
    { - onEdit(); - onClose(); - }} - > - {t("edit_link")} -
    +
    +
    { + onEdit(); + onClose(); + }} + > + {t("edit_link")}
    - - ))} +
    + + )}
    ); diff --git a/components/ModalContent/NewCollectionModal.tsx b/components/ModalContent/NewCollectionModal.tsx index df9d9bb..32cb5f9 100644 --- a/components/ModalContent/NewCollectionModal.tsx +++ b/components/ModalContent/NewCollectionModal.tsx @@ -88,7 +88,7 @@ export default function NewCollectionModal({ onClose, parent }: Props) {

    {t("color")}

    setCollection({ ...collection, color }) } @@ -96,7 +96,7 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
    { + return ( + onClose()} + className={`absolute z-50 ${className || ""}`} + > + {children} + + ); +}; + +export default Popover; diff --git a/components/ReadableView.tsx b/components/ReadableView.tsx index 18477a0..11954b6 100644 --- a/components/ReadableView.tsx +++ b/components/ReadableView.tsx @@ -205,7 +205,7 @@ export default function ReadableView({ link }: Props) { >

    diff --git a/prisma/migrations/20240819003838_add_icon_related_fields_to_link_and_collection_model/migration.sql b/prisma/migrations/20240819003838_add_icon_related_fields_to_link_and_collection_model/migration.sql new file mode 100644 index 0000000..e208342 --- /dev/null +++ b/prisma/migrations/20240819003838_add_icon_related_fields_to_link_and_collection_model/migration.sql @@ -0,0 +1,8 @@ +-- AlterTable +ALTER TABLE "Collection" ADD COLUMN "icon" TEXT, +ADD COLUMN "iconWeight" TEXT; + +-- AlterTable +ALTER TABLE "Link" ADD COLUMN "color" TEXT, +ADD COLUMN "icon" TEXT, +ADD COLUMN "iconWeight" TEXT; diff --git a/prisma/migrations/20240819005010_remove_collection_model_default_color/migration.sql b/prisma/migrations/20240819005010_remove_collection_model_default_color/migration.sql new file mode 100644 index 0000000..199e104 --- /dev/null +++ b/prisma/migrations/20240819005010_remove_collection_model_default_color/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Collection" ALTER COLUMN "color" DROP NOT NULL, +ALTER COLUMN "color" DROP DEFAULT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 71b5b10..664dec5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -94,7 +94,9 @@ model Collection { id Int @id @default(autoincrement()) name String description String @default("") - color String @default("#0ea5e9") + icon String? + iconWeight String? + color String? parentId Int? parent Collection? @relation("SubCollections", fields: [parentId], references: [id]) subCollections Collection[] @relation("SubCollections") @@ -133,6 +135,9 @@ model Link { collection Collection @relation(fields: [collectionId], references: [id]) collectionId Int tags Tag[] + icon String? + iconWeight String? + color String? url String? textContent String? preview String? diff --git a/styles/globals.css b/styles/globals.css index 1f97b6e..022c160 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -36,6 +36,14 @@ scrollbar-width: none; } +.hide-color-picker { + opacity: 0; + display: block; + width: 32px; + height: 32px; + border: none; +} + .hyphens { hyphens: auto; } From accbd4cbfa8c12b764abb0ceceb1c818804cd06f Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Mon, 19 Aug 2024 23:53:43 -0400 Subject: [PATCH 030/113] bug fixes --- components/InstallApp.tsx | 2 +- pages/api/v1/auth/[...nextauth].ts | 70 +++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/components/InstallApp.tsx b/components/InstallApp.tsx index f191f00..50071da 100644 --- a/components/InstallApp.tsx +++ b/components/InstallApp.tsx @@ -8,7 +8,7 @@ const InstallApp = (props: Props) => { const [isOpen, setIsOpen] = useState(true); return isOpen && !isPWA() ? ( -

    +
    0) { + await prisma.user.update({ + where: { + id: userExists.id, + }, + data: { + emailVerified: new Date(), + }, + }); + } + + if (userExists && !userExists.username) { const autoGeneratedUsername = "user" + Math.round(Math.random() * 1000000000); @@ -1217,6 +1264,22 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) { }, }); } + } else if (trigger === "signIn") { + const user = await prisma.user.findUnique({ + where: { + id: token.id, + }, + }); + + if (user && !user.username) { + const autoGeneratedUsername = + "user" + Math.round(Math.random() * 1000000000); + + await prisma.user.update({ + where: { id: user.id }, + data: { username: autoGeneratedUsername }, + }); + } } return token; @@ -1224,6 +1287,8 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) { async session({ session, token }) { session.user.id = token.id; + console.log("session", session); + if (STRIPE_SECRET_KEY) { const user = await prisma.user.findUnique({ where: { @@ -1235,6 +1300,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) { }); if (user) { + // const subscribedUser = await verifySubscription(user); } } From ae2324ecd352acfc215a6222727bacc34c80f444 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 20 Aug 2024 16:59:01 -0400 Subject: [PATCH 031/113] progressed icon picker component --- components/IconPicker.tsx | 168 ++++++++++-------- .../ModalContent/EditCollectionModal.tsx | 73 +++----- pages/_app.tsx | 7 +- styles/globals.css | 9 +- 4 files changed, 130 insertions(+), 127 deletions(-) diff --git a/components/IconPicker.tsx b/components/IconPicker.tsx index f90dc1f..513da2c 100644 --- a/components/IconPicker.tsx +++ b/components/IconPicker.tsx @@ -1,31 +1,26 @@ import { icons } from "@/lib/client/icons"; -import React, { useMemo, useState } from "react"; +import React, { useMemo, useState, lazy, Suspense } from "react"; import Fuse from "fuse.js"; import TextInput from "./TextInput"; import Popover from "./Popover"; import { HexColorPicker } from "react-colorful"; +import { useTranslation } from "next-i18next"; import Icon from "./Icon"; - -const fuse = new Fuse(icons, { - keys: [{ name: "name", weight: 4 }, "tags", "categories"], - threshold: 0.2, - useExtendedSearch: true, -}); +import { IconWeight } from "@phosphor-icons/react"; type Props = { - onClose: Function; - alignment?: "left" | "right" | "bottom" | "top"; + alignment?: "left" | "right"; color: string; setColor: Function; - iconName: string; + iconName?: string; setIconName: Function; - weight: "light" | "regular" | "bold" | "fill" | "duotone"; + weight: "light" | "regular" | "bold" | "fill" | "duotone" | "thin"; setWeight: Function; + reset: Function; className?: string; }; const IconPicker = ({ - onClose, alignment, color, setColor, @@ -34,8 +29,17 @@ const IconPicker = ({ weight, setWeight, className, + reset, }: Props) => { + const fuse = new Fuse(icons, { + keys: [{ name: "name", weight: 4 }, "tags", "categories"], + threshold: 0.2, + useExtendedSearch: true, + }); + + const { t } = useTranslation(); const [query, setQuery] = useState(""); + const [iconPicker, setIconPicker] = useState(false); const filteredQueryResultsSelector = useMemo(() => { if (!query) { @@ -45,67 +49,87 @@ const IconPicker = ({ }, [query]); return ( - -
    -
    -
    - - - -
    - setColor(e)} /> -
    - - setQuery(e.target.value)} - /> - -
    - {filteredQueryResultsSelector.map((icon) => { - const IconComponent = icon.Icon; - return ( -
    setIconName(icon.pascal_name)} - className={`cursor-pointer btn p-1 box-border ${ - icon.pascal_name === iconName - ? "outline outline-1 outline-primary" - : "" - }`} - > - -
    - ); - })} -
    +
    +
    setIconPicker(!iconPicker)} + className="btn btn-square w-20 h-20" + > + {iconName ? ( + + ) : ( + + )}
    - + {iconPicker && ( + setIconPicker(false)} + className={ + className + + " fade-in bg-base-200 border border-neutral-content p-2 h-44 w-[22.5rem] rounded-lg backdrop-blur-sm bg-opacity-90 top-20 left-0 lg:-translate-x-1/3" + } + > +
    +
    +
    } + > + {t("reset")} +
    + + setColor(e)} /> +
    + +
    + setQuery(e.target.value)} + /> + +
    + {filteredQueryResultsSelector.map((icon) => { + const IconComponent = icon.Icon; + return ( +
    setIconName(icon.pascal_name)} + className={`cursor-pointer btn p-1 box-border bg-base-100 border-none aspect-square ${ + icon.pascal_name === iconName + ? "outline outline-1 outline-primary" + : "" + }`} + > + +
    + ); + })} +
    +
    +
    +
    + )} +
    ); }; diff --git a/components/ModalContent/EditCollectionModal.tsx b/components/ModalContent/EditCollectionModal.tsx index 76833e7..20a6cdd 100644 --- a/components/ModalContent/EditCollectionModal.tsx +++ b/components/ModalContent/EditCollectionModal.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; import TextInput from "@/components/TextInput"; -import { HexColorPicker } from "react-colorful"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; @@ -8,6 +7,7 @@ import { useUpdateCollection } from "@/hooks/store/collections"; import toast from "react-hot-toast"; import IconPicker from "../IconPicker"; import Icon from "../Icon"; +import { IconWeight } from "@phosphor-icons/react"; type Props = { onClose: Function; @@ -22,7 +22,6 @@ export default function EditCollectionModal({ const [collection, setCollection] = useState(activeCollection); - const [iconPicker, setIconPicker] = useState(false); const [submitLoader, setSubmitLoader] = useState(false); const updateCollection = useUpdateCollection(); @@ -59,10 +58,32 @@ export default function EditCollectionModal({
    -
    -
    -

    {t("name")}

    -
    +
    +
    + + setCollection({ ...collection, color }) + } + weight={(collection.iconWeight || "regular") as IconWeight} + setWeight={(iconWeight: string) => + setCollection({ ...collection, iconWeight }) + } + iconName={collection.icon as string} + setIconName={(icon: string) => + setCollection({ ...collection, icon }) + } + reset={() => + setCollection({ + ...collection, + color: "#0ea5e9", + icon: "", + iconWeight: "", + }) + } + /> +
    +

    {t("name")}

    -
    -

    {t("color")}

    -
    -
    - setIconPicker(true)} - /> - {iconPicker && ( - setIconPicker(false)} - className="top-20" - color={collection.color as string} - setColor={(color: string) => - setCollection({ ...collection, color }) - } - weight={collection.iconWeight as any} - setWeight={(iconWeight: string) => - setCollection({ ...collection, iconWeight }) - } - iconName={collection.icon as string} - setIconName={(icon: string) => - setCollection({ ...collection, icon }) - } - /> - )} -
    - setCollection({ ...collection, color: "#0ea5e9" }) - } - > - {t("reset")} -
    -
    -
    -
    diff --git a/pages/_app.tsx b/pages/_app.tsx index dc6603a..c99d26a 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -88,13 +88,10 @@ function App({ {icon} {message} {t.type !== "loading" && ( - + >
    )}
    )} diff --git a/styles/globals.css b/styles/globals.css index 022c160..28a0105 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -144,15 +144,16 @@ /* For react-colorful */ .color-picker .react-colorful { - width: 100%; - height: 7.5rem; + height: 7rem; + width: 7rem; } .color-picker .react-colorful__hue { height: 1rem; } .color-picker .react-colorful__pointer { - width: 1.3rem; - height: 1.3rem; + width: 1rem; + height: 1rem; + border-width: 1px; } /* For the Link banner */ From 6df2e44213ce028026d9b0c1266999e79bc51daf Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 20 Aug 2024 18:11:20 -0400 Subject: [PATCH 032/113] added translation to icon picker component + other fixes and improvements --- components/IconGrid.tsx | 45 +++++++++++++++++++++++++++ components/IconPicker.tsx | 57 +++++++++++------------------------ public/locales/en/common.json | 9 +++++- 3 files changed, 70 insertions(+), 41 deletions(-) create mode 100644 components/IconGrid.tsx diff --git a/components/IconGrid.tsx b/components/IconGrid.tsx new file mode 100644 index 0000000..edb2362 --- /dev/null +++ b/components/IconGrid.tsx @@ -0,0 +1,45 @@ +import { icons } from "@/lib/client/icons"; +import Fuse from "fuse.js"; +import { useMemo } from "react"; + +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 filteredQueryResultsSelector = useMemo(() => { + if (!query) { + return icons; + } + return fuse.search(query).map((result) => result.item); + }, [query]); + + return filteredQueryResultsSelector.map((icon) => { + const IconComponent = icon.Icon; + return ( +
    setIconName(icon.pascal_name)} + className={`cursor-pointer btn p-1 box-border bg-base-100 border-none w-full ${ + icon.pascal_name === iconName + ? "outline outline-1 outline-primary" + : "" + }`} + > + +
    + ); + }); +}; + +export default IconGrid; diff --git a/components/IconPicker.tsx b/components/IconPicker.tsx index 513da2c..a712262 100644 --- a/components/IconPicker.tsx +++ b/components/IconPicker.tsx @@ -1,12 +1,11 @@ -import { icons } from "@/lib/client/icons"; -import React, { useMemo, useState, lazy, Suspense } from "react"; -import Fuse from "fuse.js"; +import React, { useState } from "react"; import TextInput from "./TextInput"; import Popover from "./Popover"; import { HexColorPicker } from "react-colorful"; import { useTranslation } from "next-i18next"; import Icon from "./Icon"; import { IconWeight } from "@phosphor-icons/react"; +import IconGrid from "./IconGrid"; type Props = { alignment?: "left" | "right"; @@ -31,23 +30,10 @@ const IconPicker = ({ className, reset, }: Props) => { - const fuse = new Fuse(icons, { - keys: [{ name: "name", weight: 4 }, "tags", "categories"], - threshold: 0.2, - useExtendedSearch: true, - }); - const { t } = useTranslation(); const [query, setQuery] = useState(""); const [iconPicker, setIconPicker] = useState(false); - const filteredQueryResultsSelector = useMemo(() => { - if (!query) { - return icons; - } - return fuse.search(query).map((result) => result.item); - }, [query]); - return (
    setWeight(e.target.value)} > - - - - - - + + + + + + setColor(e)} />
    -
    +
    setQuery(e.target.value)} />
    - {filteredQueryResultsSelector.map((icon) => { - const IconComponent = icon.Icon; - return ( -
    setIconName(icon.pascal_name)} - className={`cursor-pointer btn p-1 box-border bg-base-100 border-none aspect-square ${ - icon.pascal_name === iconName - ? "outline outline-1 outline-primary" - : "" - }`} - > - -
    - ); - })} +
    diff --git a/public/locales/en/common.json b/public/locales/en/common.json index c2e9a82..6165806 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -372,5 +372,12 @@ "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", - "notes": "Notes" + "notes": "Notes", + "regular": "Regular", + "thin": "Thin", + "bold": "Bold", + "fill": "Fill", + "duotone": "Duotone", + "light_icon": "Light", + "search": "Search" } \ No newline at end of file From bf1a6efd2e20887241c1174a1cca4538b71e179b Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 20 Aug 2024 19:25:35 -0400 Subject: [PATCH 033/113] custom icons fully implemented for collections --- components/CollectionListing.tsx | 27 +++++++-- components/LinkDetails.tsx | 19 ++++-- .../LinkComponents/LinkCollection.tsx | 19 ++++-- .../ModalContent/EditCollectionModal.tsx | 1 - .../ModalContent/NewCollectionModal.tsx | 58 +++++++++---------- components/ReadableView.tsx | 21 +++++-- .../collectionId/updateCollectionById.ts | 4 +- .../controllers/collections/postCollection.ts | 2 + pages/collections/[id].tsx | 21 +++++-- .../migration.sql | 9 +++ prisma/schema.prisma | 2 +- 11 files changed, 127 insertions(+), 56 deletions(-) create mode 100644 prisma/migrations/20240820230618_add_default_value_for_collection_color/migration.sql diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index f3a722e..1533c3b 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -17,6 +17,8 @@ import toast from "react-hot-toast"; import { useTranslation } from "next-i18next"; import { useCollections, useUpdateCollection } from "@/hooks/store/collections"; import { useUpdateUser, useUser } from "@/hooks/store/user"; +import Icon from "./Icon"; +import { IconWeight } from "@phosphor-icons/react"; interface ExtendedTreeItem extends TreeItem { data: Collection; @@ -256,7 +258,7 @@ const renderItem = ( : "hover:bg-neutral/20" } duration-100 flex gap-1 items-center pr-2 pl-1 rounded-md`} > - {Icon(item as ExtendedTreeItem, onExpand, onCollapse)} + {Dropdown(item as ExtendedTreeItem, onExpand, onCollapse)} - + {collection.icon ? ( + + ) : ( + + )} +

    {collection.name}

    {collection.isPublic && ( @@ -288,7 +301,7 @@ const renderItem = ( ); }; -const Icon = ( +const Dropdown = ( item: ExtendedTreeItem, onExpand: (id: ItemId) => void, onCollapse: (id: ItemId) => void @@ -332,6 +345,8 @@ const buildTreeFromCollections = ( name: collection.name, description: collection.description, color: collection.color, + icon: collection.icon, + iconWeight: collection.iconWeight, isPublic: collection.isPublic, ownerId: collection.ownerId, createdAt: collection.createdAt, diff --git a/components/LinkDetails.tsx b/components/LinkDetails.tsx index 0f237ca..0b7ca14 100644 --- a/components/LinkDetails.tsx +++ b/components/LinkDetails.tsx @@ -20,6 +20,8 @@ import { useGetLink } 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"; type Props = { className?: string; @@ -163,10 +165,19 @@ export default function LinkDetails({ className, link }: Props) { >

    {link.collection.name}

    - + {link.collection.icon ? ( + + ) : ( + + )}
    diff --git a/components/LinkViews/LinkComponents/LinkCollection.tsx b/components/LinkViews/LinkComponents/LinkCollection.tsx index f6e508f..45c1725 100644 --- a/components/LinkViews/LinkComponents/LinkCollection.tsx +++ b/components/LinkViews/LinkComponents/LinkCollection.tsx @@ -1,7 +1,9 @@ +import Icon from "@/components/Icon"; import { CollectionIncludingMembersAndLinkCount, LinkIncludingShortenedCollectionAndTags, } from "@/types/global"; +import { IconWeight } from "@phosphor-icons/react"; import Link from "next/link"; import React from "react"; @@ -22,10 +24,19 @@ export default function LinkCollection({ className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100 select-none" title={collection?.name} > - + {link.collection.icon ? ( + + ) : ( + + )}

    {collection?.name}

    diff --git a/components/ModalContent/EditCollectionModal.tsx b/components/ModalContent/EditCollectionModal.tsx index 20a6cdd..e5f81da 100644 --- a/components/ModalContent/EditCollectionModal.tsx +++ b/components/ModalContent/EditCollectionModal.tsx @@ -6,7 +6,6 @@ import { useTranslation } from "next-i18next"; import { useUpdateCollection } from "@/hooks/store/collections"; import toast from "react-hot-toast"; import IconPicker from "../IconPicker"; -import Icon from "../Icon"; import { IconWeight } from "@phosphor-icons/react"; type Props = { diff --git a/components/ModalContent/NewCollectionModal.tsx b/components/ModalContent/NewCollectionModal.tsx index 32cb5f9..292a8d0 100644 --- a/components/ModalContent/NewCollectionModal.tsx +++ b/components/ModalContent/NewCollectionModal.tsx @@ -1,12 +1,13 @@ import React, { useEffect, useState } from "react"; import TextInput from "@/components/TextInput"; -import { HexColorPicker } from "react-colorful"; import { Collection } from "@prisma/client"; import Modal from "../Modal"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import { useTranslation } from "next-i18next"; import { useCreateCollection } from "@/hooks/store/collections"; import toast from "react-hot-toast"; +import IconPicker from "../IconPicker"; +import { IconWeight } from "@phosphor-icons/react"; type Props = { onClose: Function; @@ -72,10 +73,32 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
    -
    -
    -

    {t("name")}

    -
    +
    +
    + + setCollection({ ...collection, color }) + } + weight={(collection.iconWeight || "regular") as IconWeight} + setWeight={(iconWeight: string) => + setCollection({ ...collection, iconWeight }) + } + iconName={collection.icon as string} + setIconName={(icon: string) => + setCollection({ ...collection, icon }) + } + reset={() => + setCollection({ + ...collection, + color: "#0ea5e9", + icon: "", + iconWeight: "", + }) + } + /> +
    +

    {t("name")}

    -
    -

    {t("color")}

    -
    - - setCollection({ ...collection, color }) - } - /> -
    - -
    - setCollection({ ...collection, color: "#0ea5e9" }) - } - > - {t("reset")} -
    -
    -
    -
    diff --git a/components/ReadableView.tsx b/components/ReadableView.tsx index 11954b6..b352489 100644 --- a/components/ReadableView.tsx +++ b/components/ReadableView.tsx @@ -16,6 +16,8 @@ import LinkActions from "./LinkViews/LinkComponents/LinkActions"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; import { useGetLink } from "@/hooks/store/links"; +import { IconWeight } from "@phosphor-icons/react"; +import Icon from "./Icon"; type LinkContent = { title: string; @@ -203,10 +205,21 @@ export default function ReadableView({ link }: Props) { href={`/collections/${link?.collection.id}`} className="flex items-center gap-1 cursor-pointer hover:opacity-60 duration-100 mr-2 z-10" > - + {link.collection.icon ? ( + + ) : ( + + )}

    - + {activeCollection.icon ? ( + + ) : ( + + )}

    {activeCollection?.name} diff --git a/prisma/migrations/20240820230618_add_default_value_for_collection_color/migration.sql b/prisma/migrations/20240820230618_add_default_value_for_collection_color/migration.sql new file mode 100644 index 0000000..f634b68 --- /dev/null +++ b/prisma/migrations/20240820230618_add_default_value_for_collection_color/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Made the column `color` on table `Collection` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Collection" ALTER COLUMN "color" SET NOT NULL, +ALTER COLUMN "color" SET DEFAULT '#0ea5e9'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 664dec5..922480f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -96,7 +96,7 @@ model Collection { description String @default("") icon String? iconWeight String? - color String? + color String @default("#0ea5e9") parentId Int? parent Collection? @relation("SubCollections", fields: [parentId], references: [id]) subCollections Collection[] @relation("SubCollections") From ed7b268c2bd54f856736942514c6460684da6aff Mon Sep 17 00:00:00 2001 From: vlad11 Date: Thu, 22 Aug 2024 03:07:25 +0300 Subject: [PATCH 034/113] Created Ukranian Translate. Signed-off-by: vlad11 --- public/locales/uk/common.json | 375 ++++++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 public/locales/uk/common.json diff --git a/public/locales/uk/common.json b/public/locales/uk/common.json new file mode 100644 index 0000000..285cf7c --- /dev/null +++ b/public/locales/uk/common.json @@ -0,0 +1,375 @@ +{ + "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>Загальні положення та умови та <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.", + "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_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": "Дата (cпочатку найновіші)", + "date_oldest_first": "Дата (спочатку найдавніші)", + "name_az": "Ім'я, зростання за алфавітом (А-Я)", + "name_za": "Ім'я, спадання за алфавітом (Я-А)", + "description_az": "Опис, зростання за алфавітом (A-Z)", + "description_za": "Опис, спадання за алфавітом (Z-A)", + "all_rights_reserved": "© {{date}} <0>Linkwarden. Всі права захищені.", + "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>Докладніше", + "full_content": "Повний вміст", + "slower": "повільніше", + "new_version_announcement": "Подивіться, що нового в <0>Linkwarden {{version}}!", + "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>Примітка: переконайтеся, що ви повідомили користувача, що йому потрібно змінити свій пароль.", + "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": "Скинути", + "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_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": "Ви впевнені, що хочете видалити ці посилання? ", + "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": "Увійдіть як демо-користувач" +} \ No newline at end of file From 9fe829771d2b6fa5102ebc1e4fa496d1d5e3a573 Mon Sep 17 00:00:00 2001 From: Isaac Wise Date: Thu, 22 Aug 2024 17:09:14 -0500 Subject: [PATCH 035/113] Add new collection drop down --- pages/collections/index.tsx | 38 +++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/pages/collections/index.tsx b/pages/collections/index.tsx index 5975923..b1dbb8b 100644 --- a/pages/collections/index.tsx +++ b/pages/collections/index.tsx @@ -10,6 +10,7 @@ import PageHeader from "@/components/PageHeader"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { dropdownTriggerer } from "@/lib/client/utils"; export default function Collections() { const { t } = useTranslation(); @@ -29,12 +30,37 @@ export default function Collections() {

    - - +
    + +
    +
    +
    + +
    +
      +
    • +
      setNewCollectionModal(true)} + className="whitespace-nowrap" + > + {t("new_collection")} +
      +
    • +
    +
    +
    +
    From fae9e95fa9962abf80e90585c2706cda58d03da2 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Sat, 24 Aug 2024 15:50:29 -0400 Subject: [PATCH 036/113] added custom icons for links --- components/IconPicker.tsx | 9 ++- components/LinkDetails.tsx | 4 +- .../LinkViews/LinkComponents/LinkIcon.tsx | 66 +++++++++---------- .../LinkViews/LinkComponents/LinkList.tsx | 2 +- components/ModalContent/EditLinkModal.tsx | 25 +++++++ .../links/linkId/updateLinkById.ts | 3 + public/locales/en/common.json | 3 +- 7 files changed, 72 insertions(+), 40 deletions(-) diff --git a/components/IconPicker.tsx b/components/IconPicker.tsx index a712262..f561137 100644 --- a/components/IconPicker.tsx +++ b/components/IconPicker.tsx @@ -8,13 +8,14 @@ import { IconWeight } from "@phosphor-icons/react"; import IconGrid from "./IconGrid"; type Props = { - alignment?: "left" | "right"; + alignment?: string; color: string; setColor: Function; iconName?: string; setIconName: Function; weight: "light" | "regular" | "bold" | "fill" | "duotone" | "thin"; setWeight: Function; + hideDefaultIcon?: boolean; reset: Function; className?: string; }; @@ -27,6 +28,7 @@ const IconPicker = ({ setIconName, weight, setWeight, + hideDefaultIcon, className, reset, }: Props) => { @@ -47,6 +49,8 @@ const IconPicker = ({ weight={(weight || "regular") as IconWeight} color={color || "#0ea5e9"} /> + ) : !iconName && hideDefaultIcon ? ( +

    {t("set_custom_icon")}

    ) : ( setIconPicker(false)} className={ className + - " fade-in bg-base-200 border border-neutral-content p-2 h-44 w-[22.5rem] rounded-lg backdrop-blur-sm bg-opacity-90 top-20 left-0 lg:-translate-x-1/3" + " fade-in bg-base-200 border border-neutral-content p-2 h-44 w-[22.5rem] rounded-lg backdrop-blur-sm bg-opacity-90 " + + (alignment || " lg:-translate-x-1/3 top-20 left-0 ") } >
    diff --git a/components/LinkDetails.tsx b/components/LinkDetails.tsx index 0b7ca14..84aabe9 100644 --- a/components/LinkDetails.tsx +++ b/components/LinkDetails.tsx @@ -127,7 +127,9 @@ export default function LinkDetails({ className, link }: Props) { return (
    - +
    + +
    {link.name &&

    {link.name}

    } diff --git a/components/LinkViews/LinkComponents/LinkIcon.tsx b/components/LinkViews/LinkComponents/LinkIcon.tsx index e60c23a..2ebc530 100644 --- a/components/LinkViews/LinkComponents/LinkIcon.tsx +++ b/components/LinkViews/LinkComponents/LinkIcon.tsx @@ -2,34 +2,24 @@ import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import Image from "next/image"; import isValidUrl from "@/lib/shared/isValidUrl"; import React 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, }: { link: LinkIncludingShortenedCollectionAndTags; className?: string; - size?: "small" | "medium"; + hideBackground?: boolean; }) { - 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-md flex item-center justify-center select-none z-10 w-12 h-12", + !hideBackground && "bg-white backdrop-blur-lg bg-opacity-50 p-1", + className + ); const url = isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined; @@ -38,14 +28,22 @@ export default function LinkIcon({ return ( <> - {link.type === "url" && url ? ( + {link.icon ? ( + + ) : link.type === "url" && url ? ( showFavicon ? ( { setShowFavicon(false); @@ -53,22 +51,22 @@ export default function LinkIcon({ /> ) : ( ) ) : link.type === "pdf" ? ( ) : link.type === "image" ? ( ) : // : link.type === "monolith" ? ( // { return ( -
    +
    ); }; + +// `text-black aspect-square text-4xl ${iconClasses}` diff --git a/components/LinkViews/LinkComponents/LinkList.tsx b/components/LinkViews/LinkComponents/LinkList.tsx index 31a214b..2eddc05 100644 --- a/components/LinkViews/LinkComponents/LinkList.tsx +++ b/components/LinkViews/LinkComponents/LinkList.tsx @@ -111,7 +111,7 @@ export default function LinkCardCompact({ } >
    - +
    diff --git a/components/ModalContent/EditLinkModal.tsx b/components/ModalContent/EditLinkModal.tsx index 07911f4..fa33870 100644 --- a/components/ModalContent/EditLinkModal.tsx +++ b/components/ModalContent/EditLinkModal.tsx @@ -9,6 +9,8 @@ import Modal from "../Modal"; import { useTranslation } from "next-i18next"; import { useUpdateLink } from "@/hooks/store/links"; import toast from "react-hot-toast"; +import IconPicker from "../IconPicker"; +import { IconWeight } from "@phosphor-icons/react"; type Props = { onClose: Function; @@ -138,6 +140,29 @@ export default function EditLinkModal({ onClose, activeLink }: Props) { className="resize-none w-full rounded-md p-2 border-neutral-content bg-base-200 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100" />
    + +
    + 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: "", + }) + } + alignment="-top-10 translate-x-20" + /> +
    diff --git a/lib/api/controllers/links/linkId/updateLinkById.ts b/lib/api/controllers/links/linkId/updateLinkById.ts index 306696a..53daa87 100644 --- a/lib/api/controllers/links/linkId/updateLinkById.ts +++ b/lib/api/controllers/links/linkId/updateLinkById.ts @@ -96,6 +96,9 @@ export default async function updateLinkById( data: { name: data.name, description: data.description, + icon: data.icon, + iconWeight: data.iconWeight, + color: data.color, collection: { connect: { id: data.collection.id, diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 6165806..9a9cc86 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -379,5 +379,6 @@ "fill": "Fill", "duotone": "Duotone", "light_icon": "Light", - "search": "Search" + "search": "Search", + "set_custom_icon": "Set Custom Icon" } \ No newline at end of file From f368c2aa81c4700e056fbfb9772edb71c6f19e24 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Mon, 26 Aug 2024 16:11:02 -0400 Subject: [PATCH 037/113] less padding for list view --- components/LinkViews/LinkComponents/LinkIcon.tsx | 14 +++----------- components/LinkViews/LinkComponents/LinkList.tsx | 13 ++++--------- components/LinkViews/Links.tsx | 14 +++++++------- components/ToggleDarkMode.tsx | 6 ++++-- pages/public/links/[id].tsx | 4 +--- 5 files changed, 19 insertions(+), 32 deletions(-) diff --git a/components/LinkViews/LinkComponents/LinkIcon.tsx b/components/LinkViews/LinkComponents/LinkIcon.tsx index 2ebc530..c7c42dd 100644 --- a/components/LinkViews/LinkComponents/LinkIcon.tsx +++ b/components/LinkViews/LinkComponents/LinkIcon.tsx @@ -16,8 +16,8 @@ export default function LinkIcon({ hideBackground?: boolean; }) { let iconClasses: string = clsx( - "rounded-md flex item-center justify-center select-none z-10 w-12 h-12", - !hideBackground && "bg-white backdrop-blur-lg bg-opacity-50 p-1", + "rounded flex item-center justify-center select-none z-10 w-12 h-12", + !hideBackground && "rounded-md bg-white backdrop-blur-lg bg-opacity-50 p-1", className ); @@ -50,23 +50,17 @@ export default function LinkIcon({ }} /> ) : ( - + ) ) : link.type === "pdf" ? ( ) : link.type === "image" ? ( ) : // : link.type === "monolith" ? ( // { return (
    diff --git a/components/LinkViews/LinkComponents/LinkList.tsx b/components/LinkViews/LinkComponents/LinkList.tsx index 2eddc05..d4e32c7 100644 --- a/components/LinkViews/LinkComponents/LinkList.tsx +++ b/components/LinkViews/LinkComponents/LinkList.tsx @@ -93,9 +93,9 @@ export default function LinkCardCompact({ return ( <>
    selectable ? handleCheckboxClick(link) @@ -143,12 +143,7 @@ export default function LinkCardCompact({ flipDropdown={flipDropdown} />
    -
    +
    ); } diff --git a/components/LinkViews/Links.tsx b/components/LinkViews/Links.tsx index 511c1d2..c8de1e5 100644 --- a/components/LinkViews/Links.tsx +++ b/components/LinkViews/Links.tsx @@ -142,7 +142,7 @@ export function ListView({ placeHolderRef?: any; }) { return ( -
    +
    {links?.map((e, i) => { return ( -
    -
    -
    -
    -
    +
    +
    +
    +
    +
    ); diff --git a/components/ToggleDarkMode.tsx b/components/ToggleDarkMode.tsx index 28631b9..6a9a5dd 100644 --- a/components/ToggleDarkMode.tsx +++ b/components/ToggleDarkMode.tsx @@ -1,12 +1,14 @@ import useLocalSettingsStore from "@/store/localSettings"; import { useEffect, useState, ChangeEvent } from "react"; import { useTranslation } from "next-i18next"; +import clsx from "clsx"; type Props = { className?: string; + align?: "left" | "right"; }; -export default function ToggleDarkMode({ className }: Props) { +export default function ToggleDarkMode({ className, align }: Props) { const { t } = useTranslation(); const { settings, updateSettings } = useLocalSettingsStore(); @@ -26,7 +28,7 @@ export default function ToggleDarkMode({ className }: Props) { return (
    { const router = useRouter(); const { id } = router.query; - useState; - const getLink = useGetLink(); useEffect(() => { From 642374c2e5f768098ae8c209ad378d31ff532189 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Mon, 26 Aug 2024 16:22:59 -0400 Subject: [PATCH 038/113] remove commented code --- components/ViewDropdown.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/components/ViewDropdown.tsx b/components/ViewDropdown.tsx index 6eba3fc..2333970 100644 --- a/components/ViewDropdown.tsx +++ b/components/ViewDropdown.tsx @@ -56,17 +56,6 @@ export default function ViewDropdown({ viewMode, setViewMode }: Props) { > - - {/* */}
    ); } From 9ae9c7c81ab348c32eb80dba4647a93c0e4a12a3 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Mon, 26 Aug 2024 18:47:10 -0400 Subject: [PATCH 039/113] refactored view dropdown --- .../LinkViews/LinkComponents/LinkCard.tsx | 13 +- .../LinkViews/LinkComponents/LinkMasonry.tsx | 8 +- components/ViewDropdown.tsx | 133 +++++++++++++----- lib/client/icons.ts | 6 +- public/locales/en/common.json | 7 +- store/localSettings.ts | 106 ++++++++++---- 6 files changed, 191 insertions(+), 82 deletions(-) diff --git a/components/LinkViews/LinkComponents/LinkCard.tsx b/components/LinkViews/LinkComponents/LinkCard.tsx index f1e32df..285437a 100644 --- a/components/LinkViews/LinkComponents/LinkCard.tsx +++ b/components/LinkViews/LinkComponents/LinkCard.tsx @@ -23,6 +23,7 @@ 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"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -41,6 +42,10 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { const { setSelectedLinks, selectedLinks } = useLinkStore(); + const { + settings: { show }, + } = useLocalSettingsStore(); + const { data: { data: links = [] }, } = useLinks(); @@ -166,11 +171,9 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { ) : (
    )} - {link.type !== "image" && ( -
    - -
    - )} +
    + +

    diff --git a/components/LinkViews/LinkComponents/LinkMasonry.tsx b/components/LinkViews/LinkComponents/LinkMasonry.tsx index 4dd70f1..b63019a 100644 --- a/components/LinkViews/LinkComponents/LinkMasonry.tsx +++ b/components/LinkViews/LinkComponents/LinkMasonry.tsx @@ -155,11 +155,9 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) { ) : link.preview === "unavailable" ? null : (
    )} - {link.type !== "image" && ( -
    - -
    - )} +
    + +
    {link.preview !== "unavailable" && ( diff --git a/components/ViewDropdown.tsx b/components/ViewDropdown.tsx index 2333970..a945065 100644 --- a/components/ViewDropdown.tsx +++ b/components/ViewDropdown.tsx @@ -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,53 +10,111 @@ type Props = { }; export default function ViewDropdown({ viewMode, setViewMode }: Props) { - const { updateSettings } = useLocalSettingsStore(); + const { settings, updateSettings } = useLocalSettingsStore((state) => state); + const { t } = useTranslation(); const onChangeViewMode = ( e: React.MouseEvent, - viewMode: ViewMode + mode: ViewMode ) => { - setViewMode(viewMode); + setViewMode(mode); + }; + + const toggleShowSetting = (setting: keyof typeof settings.show) => { + const newShowSettings = { + ...settings.show, + [setting]: !settings.show[setting], + }; + updateSettings({ show: newShowSettings }); }; useEffect(() => { updateSettings({ viewMode }); - }, [viewMode]); + }, [viewMode, updateSettings]); return ( -
    - - - - - + {viewMode === ViewMode.Card ? ( + + ) : viewMode === ViewMode.Masonry ? ( + + ) : ( + + )} +
    +
      +

      {t("view")}

      +
      + + + +
      +

      {t("show")}

      + {Object.entries(settings.show) + .filter((e) => + settings.viewMode === ViewMode.List // Hide tags and image checkbox in list view + ? e[0] !== "tags" && e[0] !== "image" + : settings.viewMode === ViewMode.Card // Hide tags checkbox in card view + ? e[0] !== "tags" + : true + ) + .map(([key, value]) => ( +
    • + +
    • + ))} +
    ); } diff --git a/lib/client/icons.ts b/lib/client/icons.ts index 2083bd0..e133df5 100644 --- a/lib/client/icons.ts +++ b/lib/client/icons.ts @@ -11,8 +11,8 @@ export const icons: ReadonlyArray = iconData.map((entry) => ({ Icon: Icons[entry.pascal_name as keyof typeof Icons] as Icons.Icon, })); -if (process.env.NODE_ENV === "development") { - console.log(`${icons.length} icons`); -} +// if (process.env.NODE_ENV === "development") { +// console.log(`${icons.length} icons`); +// } export const iconCount = Intl.NumberFormat("en-US").format(icons.length * 6); diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 9a9cc86..e514148 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -380,5 +380,10 @@ "duotone": "Duotone", "light_icon": "Light", "search": "Search", - "set_custom_icon": "Set Custom Icon" + "set_custom_icon": "Set Custom Icon", + "view": "View", + "show": "Show", + "image": "Image", + "icon": "Icon", + "date": "Date" } \ No newline at end of file diff --git a/store/localSettings.ts b/store/localSettings.ts index 864fa28..cd8e6ca 100644 --- a/store/localSettings.ts +++ b/store/localSettings.ts @@ -2,14 +2,24 @@ import { Sort } from "@/types/global"; import { create } from "zustand"; type LocalSettings = { - theme?: string; - viewMode?: string; + theme: string; + viewMode: string; + show: { + link: boolean; + title: boolean; + description: boolean; + image: boolean; + tags: boolean; + icon: boolean; + collection: boolean; + date: boolean; + }; sortBy?: Sort; }; type LocalSettingsStore = { settings: LocalSettings; - updateSettings: (settings: LocalSettings) => void; + updateSettings: (settings: Partial) => void; setSettings: () => void; }; @@ -17,50 +27,84 @@ const useLocalSettingsStore = create((set) => ({ settings: { theme: "", viewMode: "", + show: { + link: true, + title: true, + description: true, + image: true, + tags: true, + icon: true, + collection: true, + date: true, + }, sortBy: Sort.DateNewestFirst, }, - updateSettings: async (newSettings) => { - if ( - newSettings.theme !== undefined && - newSettings.theme !== localStorage.getItem("theme") - ) { - localStorage.setItem("theme", newSettings.theme); + updateSettings: (newSettings) => { + const { theme, viewMode, sortBy, show } = newSettings; - const localTheme = localStorage.getItem("theme") || ""; - - document.querySelector("html")?.setAttribute("data-theme", localTheme); + if (theme !== undefined && theme !== localStorage.getItem("theme")) { + localStorage.setItem("theme", theme); + document.querySelector("html")?.setAttribute("data-theme", theme); } if ( - newSettings.viewMode !== undefined && - newSettings.viewMode !== localStorage.getItem("viewMode") + viewMode !== undefined && + viewMode !== localStorage.getItem("viewMode") ) { - localStorage.setItem("viewMode", newSettings.viewMode); - - // const localTheme = localStorage.getItem("viewMode") || ""; + localStorage.setItem("viewMode", viewMode); } - if ( - newSettings.sortBy !== undefined && - newSettings.sortBy !== Number(localStorage.getItem("sortBy")) - ) { - localStorage.setItem("sortBy", newSettings.sortBy.toString()); + if (sortBy !== undefined) { + localStorage.setItem("sortBy", sortBy.toString()); } - set((state) => ({ settings: { ...state.settings, ...newSettings } })); - }, - setSettings: async () => { - if (!localStorage.getItem("theme")) { - localStorage.setItem("theme", "dark"); - } + const currentShowString = localStorage.getItem("show"); + const newShowString = show ? JSON.stringify(show) : currentShowString; - const localTheme = localStorage.getItem("theme") || ""; + if (newShowString !== currentShowString) { + localStorage.setItem("show", newShowString || ""); + } set((state) => ({ - settings: { ...state.settings, theme: localTheme }, + settings: { + ...state.settings, + ...newSettings, + show: show ? { ...state.settings.show, ...show } : state.settings.show, + }, })); + }, + setSettings: () => { + const theme = localStorage.getItem("theme") || "dark"; + localStorage.setItem("theme", theme); - document.querySelector("html")?.setAttribute("data-theme", localTheme); + const viewMode = localStorage.getItem("viewMode") || "card"; + localStorage.setItem("viewMode", viewMode); + + const storedShow = localStorage.getItem("show"); + const show = storedShow + ? JSON.parse(storedShow) + : { + link: true, + name: true, + description: true, + image: true, + tags: true, + icon: true, + collection: true, + date: true, + }; + localStorage.setItem("show", JSON.stringify(show)); + + set({ + settings: { + theme, + viewMode, + show, + sortBy: useLocalSettingsStore.getState().settings.sortBy, + }, + }); + + document.querySelector("html")?.setAttribute("data-theme", theme); }, })); From 0371695eb3635d8dd427b2538dd699a0472c6931 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Mon, 26 Aug 2024 19:56:04 -0400 Subject: [PATCH 040/113] choose to show which detail in each views --- components/Icon.tsx | 6 +- components/LinkDetails.tsx | 2 +- .../LinkViews/LinkComponents/LinkCard.tsx | 105 ++++++++-------- .../LinkViews/LinkComponents/LinkList.tsx | 35 +++--- .../LinkViews/LinkComponents/LinkMasonry.tsx | 117 +++++++++++------- components/ViewDropdown.tsx | 11 +- store/localSettings.ts | 4 +- 7 files changed, 158 insertions(+), 122 deletions(-) diff --git a/components/Icon.tsx b/components/Icon.tsx index 178b9e6..b179066 100644 --- a/components/Icon.tsx +++ b/components/Icon.tsx @@ -5,12 +5,12 @@ type Props = { icon: string; } & Icons.IconProps; -const Icon = forwardRef(({ icon, ...rest }) => { +const Icon = forwardRef(({ icon, ...rest }, ref) => { const IconComponent: any = Icons[icon as keyof typeof Icons]; if (!IconComponent) { - return <>; - } else return ; + return null; + } else return ; }); export default Icon; diff --git a/components/LinkDetails.tsx b/components/LinkDetails.tsx index 84aabe9..c9b1e59 100644 --- a/components/LinkDetails.tsx +++ b/components/LinkDetails.tsx @@ -192,7 +192,7 @@ export default function LinkDetails({ className, link }: Props) {

    {t("tags")}

    -
    +
    {link.tags.map((tag) => isPublicRoute ? (
    diff --git a/components/LinkViews/LinkComponents/LinkCard.tsx b/components/LinkViews/LinkComponents/LinkCard.tsx index 285437a..7d13b54 100644 --- a/components/LinkViews/LinkComponents/LinkCard.tsx +++ b/components/LinkViews/LinkComponents/LinkCard.tsx @@ -24,6 +24,7 @@ import { useUser } from "@/hooks/store/user"; import { useGetLink, useLinks } from "@/hooks/store/links"; import { useRouter } from "next/router"; import useLocalSettingsStore from "@/store/localSettings"; +import clsx from "clsx"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -148,64 +149,70 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { !editMode && window.open(generateLinkHref(link, user), "_blank") } > -
    -
    - {previewAvailable(link) ? ( - { - const target = e.target as HTMLElement; - target.style.display = "none"; - }} - /> - ) : link.preview === "unavailable" ? ( -
    - ) : ( -
    - )} -
    - -
    -
    -
    -
    - -
    -
    -

    - {unescapeString(link.name)} -

    - - -
    - + {show.image && (
    -
    - -
    -
    - {collection && ( - - )} -
    - +
    + {previewAvailable(link) ? ( + { + const target = e.target as HTMLElement; + target.style.display = "none"; + }} + /> + ) : link.preview === "unavailable" ? ( +
    + ) : ( +
    + )} + {show.icon && ( +
    + +
    + )}
    +
    + )} + +
    +
    + {show.name && ( +

    + {unescapeString(link.name)} +

    + )} + + {show.link && } +
    + + {(show.collection || show.date) && ( +
    +
    + +
    + {show.collection && ( +
    + +
    + )} + {show.date && } +
    +
    + )}
    diff --git a/components/LinkViews/LinkComponents/LinkList.tsx b/components/LinkViews/LinkComponents/LinkList.tsx index d4e32c7..90baa8b 100644 --- a/components/LinkViews/LinkComponents/LinkList.tsx +++ b/components/LinkViews/LinkComponents/LinkList.tsx @@ -18,6 +18,7 @@ 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"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -39,6 +40,10 @@ export default function LinkCardCompact({ const { data: user = {} } = useUser(); const { setSelectedLinks, selectedLinks } = useLinkStore(); + const { + settings: { show }, + } = useLocalSettingsStore(); + const { links } = useLinks(); useEffect(() => { @@ -105,33 +110,31 @@ export default function LinkCardCompact({ } >
    !editMode && window.open(generateLinkHref(link, user), "_blank") } > -
    - -
    + {show.icon && ( +
    + +
    + )}
    -

    - {link.name ? ( - unescapeString(link.name) - ) : ( -

    - -
    - )} -

    + {show.name && ( +

    + {unescapeString(link.name)} +

    + )}
    - {collection && ( + {show.link && } + {show.collection && ( )} - {link.name && } - + {show.date && }
    diff --git a/components/LinkViews/LinkComponents/LinkMasonry.tsx b/components/LinkViews/LinkComponents/LinkMasonry.tsx index b63019a..b59233d 100644 --- a/components/LinkViews/LinkComponents/LinkMasonry.tsx +++ b/components/LinkViews/LinkComponents/LinkMasonry.tsx @@ -22,6 +22,8 @@ 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"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -39,6 +41,10 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) { const { setSelectedLinks, selectedLinks } = useLinkStore(); + const { + settings: { show }, + } = useLocalSettingsStore(); + const { links } = useLinks(); const getLink = useGetLink(); @@ -129,55 +135,64 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) { : undefined } > -
    - !editMode && window.open(generateLinkHref(link, user), "_blank") - } - > -
    - {previewAvailable(link) ? ( - { - const target = e.target as HTMLElement; - target.style.display = "none"; - }} - /> - ) : link.preview === "unavailable" ? null : ( -
    - )} -
    - -
    -
    +
    + {show.image && previewAvailable(link) && ( +
    + !editMode && window.open(generateLinkHref(link, user), "_blank") + } + > +
    + {previewAvailable(link) ? ( + { + const target = e.target as HTMLElement; + target.style.display = "none"; + }} + /> + ) : link.preview === "unavailable" ? null : ( +
    + )} + {show.icon && ( +
    + +
    + )} +
    - {link.preview !== "unavailable" && ( -
    +
    +
    )} -
    -

    - {unescapeString(link.name)} -

    +
    + {show.name && ( +

    + {unescapeString(link.name)} +

    + )} - + {show.link && } - {link.description && ( -

    + {show.description && link.description && ( +

    {unescapeString(link.description)}

    )} - {link.tags && link.tags[0] && ( + {show.tags && link.tags && link.tags[0] && (
    {link.tags.map((e, i) => ( -
    + {(show.collection || show.date) && ( +
    +
    -
    - {collection && } - -
    +
    + {show.collection && ( +
    + +
    + )} + {show.date && } +
    +
    + )}
    diff --git a/components/ViewDropdown.tsx b/components/ViewDropdown.tsx index a945065..f06c45d 100644 --- a/components/ViewDropdown.tsx +++ b/components/ViewDropdown.tsx @@ -89,10 +89,13 @@ export default function ViewDropdown({ viewMode, setViewMode }: Props) {

    {t("show")}

    {Object.entries(settings.show) .filter((e) => - settings.viewMode === ViewMode.List // Hide tags and image checkbox in list view - ? e[0] !== "tags" && e[0] !== "image" - : settings.viewMode === ViewMode.Card // Hide tags checkbox in card view - ? e[0] !== "tags" + settings.viewMode === ViewMode.List // Hide tags, image, icon, and description checkboxes in list view + ? e[0] !== "tags" && + e[0] !== "image" && + e[0] !== "icon" && + 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]) => ( diff --git a/store/localSettings.ts b/store/localSettings.ts index cd8e6ca..9b576ce 100644 --- a/store/localSettings.ts +++ b/store/localSettings.ts @@ -6,7 +6,7 @@ type LocalSettings = { viewMode: string; show: { link: boolean; - title: boolean; + name: boolean; description: boolean; image: boolean; tags: boolean; @@ -29,7 +29,7 @@ const useLocalSettingsStore = create((set) => ({ viewMode: "", show: { link: true, - title: true, + name: true, description: true, image: true, tags: true, From 6498ae794b59f677fc7f4814c9197bcc5d87da1d Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Mon, 26 Aug 2024 21:04:52 -0400 Subject: [PATCH 041/113] custom preview initial commit --- components/Drawer.tsx | 2 +- components/Modal.tsx | 2 +- .../ModalContent/EditCollectionModal.tsx | 2 +- components/ModalContent/EditLinkModal.tsx | 74 +++++++++++++------ .../ModalContent/NewCollectionModal.tsx | 2 +- components/ModalContent/NewLinkModal.tsx | 2 +- components/ModalContent/UploadFileModal.tsx | 2 +- public/locales/en/common.json | 4 +- 8 files changed, 61 insertions(+), 29 deletions(-) diff --git a/components/Drawer.tsx b/components/Drawer.tsx index 12f931e..07673fb 100644 --- a/components/Drawer.tsx +++ b/components/Drawer.tsx @@ -40,7 +40,7 @@ export default function Drawer({ dismissible && setDrawerIsOpen(false)} > - +
    dismissible && setDrawerIsOpen(false)} > - +

    {t("description")}