From d261bd39ecabef02411baa3d2406ecf355cad795 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 4 Jun 2024 16:59:49 -0400 Subject: [PATCH] added internationalization to pages [WIP] --- components/LinkListOptions.tsx | 203 +++++++++++++++++ components/SortDropdown.tsx | 26 +-- components/UserListing.tsx | 87 ++++++++ layouts/AuthRedirect.tsx | 1 + layouts/CenteredForm.tsx | 2 +- .../controllers/links/bulk/deleteLinksById.ts | 1 - lib/client/addMemberToCollection.ts | 8 +- next-i18next.config.js | 1 + pages/admin.tsx | 77 +------ pages/api/v1/auth/[...nextauth].ts | 39 +--- pages/auth/reset-password.tsx | 33 ++- pages/auth/verify-email.tsx | 9 +- pages/collections/[id].tsx | 203 +++++------------ pages/collections/index.tsx | 19 +- pages/confirmation.tsx | 23 +- pages/dashboard.tsx | 70 ++++-- pages/forgot.tsx | 27 ++- pages/links/index.tsx | 163 ++------------ pages/links/pinned.tsx | 165 ++------------ pages/login.tsx | 120 ++++++++-- pages/public/collections/[id].tsx | 88 ++++---- pages/register.tsx | 203 +++++++++++------ pages/search.tsx | 176 ++------------- pages/settings/access-tokens.tsx | 23 +- pages/settings/account.tsx | 121 +++++----- pages/settings/billing.tsx | 16 +- pages/settings/delete.tsx | 97 ++++---- pages/settings/password.tsx | 38 ++-- pages/settings/preference.tsx | 97 ++++---- pages/subscribe.tsx | 70 ++++-- pages/tags/[id].tsx | 146 ++---------- public/locales/en/common.json | 210 +++++++++++++++++- 32 files changed, 1299 insertions(+), 1263 deletions(-) create mode 100644 components/LinkListOptions.tsx create mode 100644 components/UserListing.tsx diff --git a/components/LinkListOptions.tsx b/components/LinkListOptions.tsx new file mode 100644 index 0000000..880e91d --- /dev/null +++ b/components/LinkListOptions.tsx @@ -0,0 +1,203 @@ +import React, { Dispatch, SetStateAction, useEffect, useState } from "react"; +import FilterSearchDropdown from "./FilterSearchDropdown"; +import SortDropdown from "./SortDropdown"; +import ViewDropdown from "./ViewDropdown"; +import { TFunction } from "i18next"; +import BulkDeleteLinksModal from "./ModalContent/BulkDeleteLinksModal"; +import BulkEditLinksModal from "./ModalContent/BulkEditLinksModal"; +import toast from "react-hot-toast"; +import useCollectivePermissions from "@/hooks/useCollectivePermissions"; +import { useRouter } from "next/router"; +import useLinkStore from "@/store/links"; +import { Sort } from "@/types/global"; + +type Props = { + children: React.ReactNode; + t: TFunction<"translation", undefined>; + viewMode: string; + setViewMode: Dispatch>; + searchFilter?: { + name: boolean; + url: boolean; + description: boolean; + tags: boolean; + textContent: boolean; + }; + setSearchFilter?: (filter: { + name: boolean; + url: boolean; + description: boolean; + tags: boolean; + textContent: boolean; + }) => void; + sortBy: Sort; + setSortBy: Dispatch>; + editMode?: boolean; + setEditMode?: (mode: boolean) => void; +}; + +const LinkListOptions = ({ + children, + t, + viewMode, + setViewMode, + searchFilter, + setSearchFilter, + sortBy, + setSortBy, + editMode, + setEditMode, +}: Props) => { + const { links, selectedLinks, setSelectedLinks, deleteLinksById } = + useLinkStore(); + + const router = useRouter(); + + const [bulkDeleteLinksModal, setBulkDeleteLinksModal] = useState(false); + const [bulkEditLinksModal, setBulkEditLinksModal] = useState(false); + + useEffect(() => { + if (editMode && setEditMode) return setEditMode(false); + }, [router]); + + const collectivePermissions = useCollectivePermissions( + selectedLinks.map((link) => link.collectionId as number) + ); + + const handleSelectAll = () => { + if (selectedLinks.length === links.length) { + setSelectedLinks([]); + } else { + setSelectedLinks(links.map((link) => link)); + } + }; + + const bulkDeleteLinks = async () => { + const load = toast.loading(t("deleting_selections")); + + const response = await deleteLinksById( + selectedLinks.map((link) => link.id as number) + ); + + toast.dismiss(load); + + response.ok && + toast.success( + selectedLinks.length === 1 + ? t("link_deleted") + : t("links_deleted", { count: selectedLinks.length }) + ); + }; + + return ( + <> +
+ {children} + +
+
+ {links.length > 0 && editMode !== undefined && setEditMode && ( +
{ + setEditMode(!editMode); + setSelectedLinks([]); + }} + className={`btn btn-square btn-sm btn-ghost ${ + editMode + ? "bg-primary/20 hover:bg-primary/20" + : "hover:bg-neutral/20" + }`} + > + +
+ )} + {searchFilter && setSearchFilter && ( + + )} + + +
+
+
+ + {editMode && links.length > 0 && ( +
+ {links.length > 0 && ( +
+ handleSelectAll()} + checked={ + selectedLinks.length === links.length && links.length > 0 + } + /> + {selectedLinks.length > 0 ? ( + + {selectedLinks.length === 1 + ? t("link_selected") + : t("links_selected", { count: selectedLinks.length })} + + ) : ( + {t("nothing_selected")} + )} +
+ )} +
+ + +
+
+ )} + + {bulkDeleteLinksModal && ( + { + setBulkDeleteLinksModal(false); + }} + /> + )} + + {bulkEditLinksModal && ( + { + setBulkEditLinksModal(false); + }} + /> + )} + + ); +}; + +export default LinkListOptions; diff --git a/components/SortDropdown.tsx b/components/SortDropdown.tsx index 81d4496..002dac5 100644 --- a/components/SortDropdown.tsx +++ b/components/SortDropdown.tsx @@ -1,13 +1,15 @@ import React, { Dispatch, SetStateAction } from "react"; import { Sort } from "@/types/global"; import { dropdownTriggerer } from "@/lib/client/utils"; +import { TFunction } from "i18next"; type Props = { sortBy: Sort; setSort: Dispatch>; + t: TFunction<"translation", undefined>; }; -export default function SortDropdown({ sortBy, setSort }: Props) { +export default function SortDropdown({ sortBy, setSort, t }: Props) { return (
{ - setSort(Sort.DateNewestFirst); - }} + onChange={() => setSort(Sort.DateNewestFirst)} /> - Date (Newest First) + {t("date_newest_first")}
  • @@ -48,11 +47,10 @@ export default function SortDropdown({ sortBy, setSort }: Props) { type="radio" name="sort-radio" className="radio checked:bg-primary" - value="Date (Oldest First)" checked={sortBy === Sort.DateOldestFirst} onChange={() => setSort(Sort.DateOldestFirst)} /> - Date (Oldest First) + {t("date_oldest_first")}
  • @@ -65,11 +63,10 @@ export default function SortDropdown({ sortBy, setSort }: Props) { type="radio" name="sort-radio" className="radio checked:bg-primary" - value="Name (A-Z)" checked={sortBy === Sort.NameAZ} onChange={() => setSort(Sort.NameAZ)} /> - Name (A-Z) + {t("name_az")}
  • @@ -82,11 +79,10 @@ export default function SortDropdown({ sortBy, setSort }: Props) { type="radio" name="sort-radio" className="radio checked:bg-primary" - value="Name (Z-A)" checked={sortBy === Sort.NameZA} onChange={() => setSort(Sort.NameZA)} /> - Name (Z-A) + {t("name_za")}
  • @@ -99,11 +95,10 @@ export default function SortDropdown({ sortBy, setSort }: Props) { type="radio" name="sort-radio" className="radio checked:bg-primary" - value="Description (A-Z)" checked={sortBy === Sort.DescriptionAZ} onChange={() => setSort(Sort.DescriptionAZ)} /> - Description (A-Z) + {t("description_az")}
  • @@ -116,11 +111,10 @@ export default function SortDropdown({ sortBy, setSort }: Props) { type="radio" name="sort-radio" className="radio checked:bg-primary" - value="Description (Z-A)" checked={sortBy === Sort.DescriptionZA} onChange={() => setSort(Sort.DescriptionZA)} /> - Description (Z-A) + {t("description_za")}
  • diff --git a/components/UserListing.tsx b/components/UserListing.tsx new file mode 100644 index 0000000..378ad98 --- /dev/null +++ b/components/UserListing.tsx @@ -0,0 +1,87 @@ +import DeleteUserModal from "@/components/ModalContent/DeleteUserModal"; +import { User as U } from "@prisma/client"; +import { TFunction } from "i18next"; + +interface User extends U { + subscriptions: { + active: boolean; + }; +} + +type UserModal = { + isOpen: boolean; + userId: number | null; +}; + +const UserListing = ( + users: User[], + deleteUserModal: UserModal, + setDeleteUserModal: Function, + t: TFunction<"translation", undefined> +) => { + return ( +
    + + + + + + {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( + + )} + {process.env.NEXT_PUBLIC_STRIPE === "true" && ( + + )} + + + + + + {users.map((user, index) => ( + + + + {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( + + )} + {process.env.NEXT_PUBLIC_STRIPE === "true" && ( + + )} + + + + ))} + +
    {t("username")}{t("email")}{t("subscribed")}{t("created_at")}
    {index + 1} + {user.username ? user.username : {t("not_available")}} + {user.email} + {user.subscriptions?.active ? ( + + ) : ( + + )} + {new Date(user.createdAt).toLocaleString()} + +
    + + {deleteUserModal.isOpen && deleteUserModal.userId ? ( + setDeleteUserModal({ isOpen: false, userId: null })} + userId={deleteUserModal.userId} + /> + ) : null} +
    + ); +}; + +export default UserListing; diff --git a/layouts/AuthRedirect.tsx b/layouts/AuthRedirect.tsx index ddc5d8a..fd0f8ba 100644 --- a/layouts/AuthRedirect.tsx +++ b/layouts/AuthRedirect.tsx @@ -42,6 +42,7 @@ export default function AuthRedirect({ children }: Props) { { path: "/tags", isProtected: true }, { path: "/preserved", isProtected: true }, { path: "/admin", isProtected: true }, + { path: "/search", isProtected: true }, ]; if (isPublicPage) { diff --git a/layouts/CenteredForm.tsx b/layouts/CenteredForm.tsx index 5960ffb..ac402b8 100644 --- a/layouts/CenteredForm.tsx +++ b/layouts/CenteredForm.tsx @@ -1,7 +1,7 @@ import useLocalSettingsStore from "@/store/localSettings"; import Image from "next/image"; import Link from "next/link"; -import React, { ReactNode, useEffect } from "react"; +import React, { ReactNode } from "react"; interface Props { text?: string; diff --git a/lib/api/controllers/links/bulk/deleteLinksById.ts b/lib/api/controllers/links/bulk/deleteLinksById.ts index 2db3896..a9c395b 100644 --- a/lib/api/controllers/links/bulk/deleteLinksById.ts +++ b/lib/api/controllers/links/bulk/deleteLinksById.ts @@ -1,7 +1,6 @@ import { prisma } from "@/lib/api/db"; import { UsersAndCollections } from "@prisma/client"; import getPermission from "@/lib/api/getPermission"; -import removeFile from "@/lib/api/storage/removeFile"; import { removeFiles } from "@/lib/api/manageLinkFiles"; export default async function deleteLinksById( diff --git a/lib/client/addMemberToCollection.ts b/lib/client/addMemberToCollection.ts index 8d1e0b1..4e03720 100644 --- a/lib/client/addMemberToCollection.ts +++ b/lib/client/addMemberToCollection.ts @@ -1,12 +1,14 @@ import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global"; import getPublicUserData from "./getPublicUserData"; import { toast } from "react-hot-toast"; +import { TFunction } from "i18next"; const addMemberToCollection = async ( ownerUsername: string, memberUsername: string, collection: CollectionIncludingMembersAndLinkCount, - setMember: (newMember: Member) => null | undefined + setMember: (newMember: Member) => null | undefined, + t: TFunction<"translation", undefined> ) => { const checkIfMemberAlreadyExists = collection.members.find((e) => { const username = (e.user.username || "").toLowerCase(); @@ -39,9 +41,9 @@ const addMemberToCollection = async ( }, }); } - } else if (checkIfMemberAlreadyExists) toast.error("User already exists."); + } else if (checkIfMemberAlreadyExists) toast.error(t("user_already_member")); else if (memberUsername.trim().toLowerCase() === ownerUsername.toLowerCase()) - toast.error("You are already the collection owner."); + toast.error(t("you_are_already_collection_owner")); }; export default addMemberToCollection; diff --git a/next-i18next.config.js b/next-i18next.config.js index d74375d..a163e4c 100644 --- a/next-i18next.config.js +++ b/next-i18next.config.js @@ -4,4 +4,5 @@ module.exports = { defaultLocale: "en", locales: ["en"], }, + reloadOnPrerender: process.env.NODE_ENV === "development", }; diff --git a/pages/admin.tsx b/pages/admin.tsx index 3341fb8..a11affc 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -6,6 +6,7 @@ import Link from "next/link"; import { useEffect, useState } from "react"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; +import UserListing from "@/components/UserListing"; interface User extends U { subscriptions: { @@ -64,7 +65,7 @@ export default function Admin() { { setSearchQuery(e.target.value); @@ -95,13 +96,13 @@ export default function Admin() {
    {filteredUsers && filteredUsers.length > 0 && searchQuery !== "" ? ( - UserListing(filteredUsers, deleteUserModal, setDeleteUserModal) + UserListing(filteredUsers, deleteUserModal, setDeleteUserModal, t) ) : searchQuery !== "" ? ( -

    No users found with the given search query.

    +

    {t("no_user_found_in_search")}

    ) : users && users.length > 0 ? ( - UserListing(users, deleteUserModal, setDeleteUserModal) + UserListing(users, deleteUserModal, setDeleteUserModal, t) ) : ( -

    No users found.

    +

    {t("no_users_found")}

    )} {newUserModal ? ( @@ -111,70 +112,4 @@ export default function Admin() { ); } -const UserListing = ( - users: User[], - deleteUserModal: UserModal, - setDeleteUserModal: Function -) => { - return ( -
    - - - - - - {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( - - )} - {process.env.NEXT_PUBLIC_STRIPE === "true" && } - - - - - - {users.map((user, index) => ( - - - - {process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && ( - - )} - {process.env.NEXT_PUBLIC_STRIPE === "true" && ( - - )} - - - - ))} - -
    UsernameEmailSubscribedCreated At
    {index + 1}{user.username ? user.username : N/A}{user.email} - {user.subscriptions?.active ? ( - JSON.stringify(user.subscriptions?.active) - ) : ( - N/A - )} - {new Date(user.createdAt).toLocaleString()} - -
    - - {deleteUserModal.isOpen && deleteUserModal.userId ? ( - setDeleteUserModal({ isOpen: false, userId: null })} - userId={deleteUserModal.userId} - /> - ) : null} -
    - ); -}; - export { getServerSideProps }; diff --git a/pages/api/v1/auth/[...nextauth].ts b/pages/api/v1/auth/[...nextauth].ts index 91fe7a7..5f98479 100644 --- a/pages/api/v1/auth/[...nextauth].ts +++ b/pages/api/v1/auth/[...nextauth].ts @@ -114,44 +114,7 @@ if ( if (!user) throw Error("Invalid credentials."); else if (!user?.emailVerified && emailEnabled) { - const identifier = user?.email as string; - const token = randomBytes(32).toString("hex"); - const url = `${ - process.env.NEXTAUTH_URL - }/callback/email?token=${token}&email=${encodeURIComponent( - identifier - )}`; - const from = process.env.EMAIL_FROM as string; - - const recentVerificationRequestsCount = - await prisma.verificationToken.count({ - where: { - identifier, - createdAt: { - gt: new Date(new Date().getTime() - 1000 * 60 * 5), // 5 minutes - }, - }, - }); - - if (recentVerificationRequestsCount >= 4) - throw Error("Too many requests. Please try again later."); - - sendVerificationRequest({ - identifier, - url, - from, - token, - }); - - await prisma.verificationToken.create({ - data: { - identifier, - token, - expires: new Date(Date.now() + 24 * 3600 * 1000), // 1 day - }, - }); - - throw Error("Email not verified. Verification email sent."); + throw Error("Email not verified."); } let passwordMatches: boolean = false; diff --git a/pages/auth/reset-password.tsx b/pages/auth/reset-password.tsx index 44d6abf..9d5301e 100644 --- a/pages/auth/reset-password.tsx +++ b/pages/auth/reset-password.tsx @@ -5,6 +5,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { FormEvent, useState } from "react"; import { toast } from "react-hot-toast"; +import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useTranslation } from "next-i18next"; interface FormData { password: string; @@ -12,8 +14,8 @@ interface FormData { } export default function ResetPassword() { + const { t } = useTranslation(); const [submitLoader, setSubmitLoader] = useState(false); - const router = useRouter(); const [form, setForm] = useState({ @@ -34,7 +36,7 @@ export default function ResetPassword() { ) { setSubmitLoader(true); - const load = toast.loading("Sending password recovery link..."); + const load = toast.loading(t("sending_password_recovery_link")); const response = await fetch("/api/v1/auth/reset-password", { method: "POST", @@ -46,6 +48,7 @@ export default function ResetPassword() { const data = await response.json(); + toast.dismiss(load); if (response.ok) { toast.success(data.response); setRequestSent(true); @@ -53,11 +56,9 @@ export default function ResetPassword() { toast.error(data.response); } - toast.dismiss(load); - setSubmitLoader(false); } else { - toast.error("Please fill out all the fields."); + toast.error(t("please_fill_all_fields")); } } @@ -66,22 +67,18 @@ export default function ResetPassword() {

    - {requestSent ? "Password Updated!" : "Reset Password"} + {requestSent ? t("password_updated") : t("reset_password")}

    {!requestSent ? ( <> +

    {t("enter_email_for_new_password")}

    -

    - Enter your email so we can send you a link to create a new - password. +

    + {t("new_password")}

    -
    -
    -

    New Password

    -
    - - Update Password + {t("update_password")} ) : ( <> -

    Your password has been successfully updated.

    - +

    {t("password_successfully_updated")}

    - Back to Login + {t("back_to_login")}
    @@ -120,3 +115,5 @@ export default function ResetPassword() { ); } + +export { getServerSideProps }; diff --git a/pages/auth/verify-email.tsx b/pages/auth/verify-email.tsx index db3e920..7b8e3b1 100644 --- a/pages/auth/verify-email.tsx +++ b/pages/auth/verify-email.tsx @@ -2,11 +2,14 @@ import { signOut } from "next-auth/react"; import { useRouter } from "next/router"; import { useEffect } from "react"; import toast from "react-hot-toast"; +import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useTranslation } from "next-i18next"; const VerifyEmail = () => { const router = useRouter(); useEffect(() => { + const { t } = useTranslation(); const token = router.query.token; if (!token || typeof token !== "string") { @@ -19,12 +22,12 @@ const VerifyEmail = () => { method: "POST", }).then((res) => { if (res.ok) { - toast.success("Email verified. Signing out.."); + toast.success(t("email_verified_signing_out")); setTimeout(() => { signOut(); }, 3000); } else { - toast.error("Invalid token."); + toast.error(t("invalid_token")); } }); @@ -35,3 +38,5 @@ const VerifyEmail = () => { }; export default VerifyEmail; + +export { getServerSideProps }; diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index 10a7d2d..58d474d 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -9,7 +9,6 @@ import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import MainLayout from "@/layouts/MainLayout"; import ProfilePhoto from "@/components/ProfilePhoto"; -import SortDropdown from "@/components/SortDropdown"; import useLinks from "@/hooks/useLinks"; import usePermissions from "@/hooks/usePermissions"; import NoLinksFound from "@/components/NoLinksFound"; @@ -19,23 +18,22 @@ import getPublicUserData from "@/lib/client/getPublicUserData"; import EditCollectionModal from "@/components/ModalContent/EditCollectionModal"; import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal"; import DeleteCollectionModal from "@/components/ModalContent/DeleteCollectionModal"; -import ViewDropdown from "@/components/ViewDropdown"; import CardView from "@/components/LinkViews/Layouts/CardView"; import ListView from "@/components/LinkViews/Layouts/ListView"; import { dropdownTriggerer } from "@/lib/client/utils"; import NewCollectionModal from "@/components/ModalContent/NewCollectionModal"; -import BulkDeleteLinksModal from "@/components/ModalContent/BulkDeleteLinksModal"; -import toast from "react-hot-toast"; -import BulkEditLinksModal from "@/components/ModalContent/BulkEditLinksModal"; import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; +import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useTranslation } from "next-i18next"; +import LinkListOptions from "@/components/LinkListOptions"; export default function Index() { + const { t } = useTranslation(); const { settings } = useLocalSettingsStore(); const router = useRouter(); - const { links, selectedLinks, setSelectedLinks, deleteLinksById } = - useLinkStore(); + const { links } = useLinkStore(); const { collections } = useCollectionStore(); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); @@ -84,9 +82,6 @@ export default function Index() { }; fetchOwner(); - - // When the collection changes, reset the selected links - setSelectedLinks([]); }, [activeCollection]); const [editCollectionModal, setEditCollectionModal] = useState(false); @@ -94,8 +89,6 @@ export default function Index() { const [editCollectionSharingModal, setEditCollectionSharingModal] = useState(false); const [deleteCollectionModal, setDeleteCollectionModal] = useState(false); - const [bulkDeleteLinksModal, setBulkDeleteLinksModal] = useState(false); - const [bulkEditLinksModal, setBulkEditLinksModal] = useState(false); const [editMode, setEditMode] = useState(false); useEffect(() => { @@ -115,35 +108,6 @@ export default function Index() { // @ts-ignore const LinkComponent = linkView[viewMode]; - const handleSelectAll = () => { - if (selectedLinks.length === links.length) { - setSelectedLinks([]); - } else { - setSelectedLinks(links.map((link) => link)); - } - }; - - const bulkDeleteLinks = async () => { - const load = toast.loading( - `Deleting ${selectedLinks.length} Link${ - selectedLinks.length > 1 ? "s" : "" - }...` - ); - - const response = await deleteLinksById( - selectedLinks.map((link) => link.id as number) - ); - - toast.dismiss(load); - - response.ok && - toast.success( - `Deleted ${selectedLinks.length} Link${ - selectedLinks.length > 1 ? "s" : "" - }!` - ); - }; - return (
    - Edit Collection Info + {t("edit_collection_info")}
    )} @@ -201,8 +165,8 @@ export default function Index() { }} > {permissions === true - ? "Share and Collaborate" - : "View Team"} + ? t("share_and_collaborate") + : t("view_team")}
    {permissions === true && ( @@ -215,7 +179,7 @@ export default function Index() { setNewCollectionModal(true); }} > - Create Sub-Collection + {t("create_subcollection")}
    )} @@ -229,8 +193,8 @@ export default function Index() { }} > {permissions === true - ? "Delete Collection" - : "Leave Collection"} + ? t("delete_collection") + : t("leave_collection")}
    @@ -272,11 +236,23 @@ export default function Index() { ) : null} -

    - By {collectionOwner.name} + +

    {activeCollection.members.length > 0 && - ` and ${activeCollection.members.length} others`} - . + 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, + }) + : t("by_author", { + author: collectionOwner.name, + })}

    @@ -313,84 +289,37 @@ export default function Index() {
    -
    -

    Showing {activeCollection?._count?.links} results

    -
    - {links.length > 0 && - (permissions === true || - permissions?.canUpdate || - permissions?.canDelete) && ( -
    { - setEditMode(!editMode); - setSelectedLinks([]); - }} - className={`btn btn-square btn-sm btn-ghost ${ - editMode - ? "bg-primary/20 hover:bg-primary/20" - : "hover:bg-neutral/20" - }`} - > - -
    - )} - - -
    -
    - - {editMode && links.length > 0 && ( -
    - {links.length > 0 && ( -
    - handleSelectAll()} - checked={ - selectedLinks.length === links.length && links.length > 0 - } - /> - {selectedLinks.length > 0 ? ( - - {selectedLinks.length}{" "} - {selectedLinks.length === 1 ? "link" : "links"} selected - - ) : ( - Nothing selected - )} -
    - )} -
    - - -
    -
    - )} + +

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

    +
    {links.some((e) => e.collectionId === Number(router.query.id)) ? ( )} - {bulkDeleteLinksModal && ( - { - setBulkDeleteLinksModal(false); - }} - /> - )} - {bulkEditLinksModal && ( - { - setBulkEditLinksModal(false); - }} - /> - )} )} ); } + +export { getServerSideProps }; diff --git a/pages/collections/index.tsx b/pages/collections/index.tsx index f5ed526..642e3dd 100644 --- a/pages/collections/index.tsx +++ b/pages/collections/index.tsx @@ -8,8 +8,11 @@ import { Sort } from "@/types/global"; import useSort from "@/hooks/useSort"; import NewCollectionModal from "@/components/ModalContent/NewCollectionModal"; import PageHeader from "@/components/PageHeader"; +import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useTranslation } from "next-i18next"; export default function Collections() { + const { t } = useTranslation(); const { collections } = useCollectionStore(); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); const [sortedCollections, setSortedCollections] = useState(collections); @@ -26,13 +29,13 @@ export default function Collections() {
    - +
    @@ -48,7 +51,9 @@ export default function Collections() { className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content p-5 bg-base-200 self-stretch min-h-[12rem] rounded-2xl cursor-pointer flex flex-col gap-4 justify-center items-center group btn" onClick={() => setNewCollectionModal(true)} > -

    New Collection

    +

    + {t("new_collection")} +

    @@ -57,8 +62,8 @@ export default function Collections() { <>
    @@ -77,3 +82,5 @@ export default function Collections() { ); } + +export { getServerSideProps }; diff --git a/pages/confirmation.tsx b/pages/confirmation.tsx index f8f99b6..4e33a9c 100644 --- a/pages/confirmation.tsx +++ b/pages/confirmation.tsx @@ -1,19 +1,25 @@ import CenteredForm from "@/layouts/CenteredForm"; import { signIn } from "next-auth/react"; -import Link from "next/link"; import { useRouter } from "next/router"; import React, { useState } from "react"; import toast from "react-hot-toast"; +import { useTranslation } from "next-i18next"; +import getServerSideProps from "@/lib/client/getServerSideProps"; export default function EmailConfirmaion() { const router = useRouter(); + const { t } = useTranslation(); + const [submitLoader, setSubmitLoader] = useState(false); const resend = async () => { + if (submitLoader) return; + else if (!router.query.email) return; + setSubmitLoader(true); - const load = toast.loading("Authenticating..."); + const load = toast.loading(t("authenticating")); const res = await signIn("email", { email: decodeURIComponent(router.query.email as string), @@ -25,29 +31,28 @@ export default function EmailConfirmaion() { setSubmitLoader(false); - toast.success("Verification email sent."); + toast.success(t("verification_email_sent")); }; return (

    - Please check your Email + {t("check_your_email")}

    -

    - A sign in link has been sent to your email address. If you don't see - the email, check your spam folder. -

    +

    {t("verification_email_sent_desc")}

    - Resend Email + {t("resend_email")}
    ); } + +export { getServerSideProps }; diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index 1131ba1..3d1fdb6 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -17,8 +17,11 @@ import ListView from "@/components/LinkViews/Layouts/ListView"; import ViewDropdown from "@/components/ViewDropdown"; import { dropdownTriggerer } from "@/lib/client/utils"; import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; +import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useTranslation } from "next-i18next"; export default function Dashboard() { + const { t } = useTranslation(); const { collections } = useCollectionStore(); const { links } = useLinkStore(); const { tags } = useTagStore(); @@ -117,7 +120,7 @@ export default function Dashboard() {
    @@ -125,7 +128,7 @@ export default function Dashboard() {
    @@ -133,7 +136,9 @@ export default function Dashboard() {
    @@ -141,7 +146,7 @@ export default function Dashboard() {
    @@ -152,15 +157,15 @@ export default function Dashboard() {
    - View All + {t("view_all")}
    @@ -176,11 +181,10 @@ export default function Dashboard() { ) : (

    - View Your Recently Added Links Here! + {t("view_added_links_here")}

    - This section will view your latest added Links across every - Collections you have access to. + {t("view_added_links_here_desc")}

    @@ -192,7 +196,7 @@ export default function Dashboard() { > - Add New Link + {t("add_link")}
    @@ -205,7 +209,7 @@ export default function Dashboard() { id="import-dropdown" > -

    Import From

    +

    {t("import_links")}

    • @@ -213,9 +217,9 @@ export default function Dashboard() { tabIndex={0} role="button" htmlFor="import-linkwarden-file" - title="JSON File" + title={t("from_linkwarden")} > - From Linkwarden + {t("from_linkwarden")} - From Bookmarks HTML file + {t("from_html")}
    • +
    • + +
    @@ -259,15 +283,15 @@ export default function Dashboard() {
    - View All + {t("view_all")} @@ -291,12 +315,10 @@ export default function Dashboard() { >

    - Pin Your Favorite Links Here! + {t("pin_favorite_links_here")}

    - You can Pin your favorite Links by clicking on the three dots on - each Link and clicking{" "} - Pin to Dashboard. + {t("pin_favorite_links_here_desc")}

    )} @@ -308,3 +330,5 @@ export default function Dashboard() { ); } + +export { getServerSideProps }; diff --git a/pages/forgot.tsx b/pages/forgot.tsx index ff2ce9c..7b01d7a 100644 --- a/pages/forgot.tsx +++ b/pages/forgot.tsx @@ -4,12 +4,15 @@ import CenteredForm from "@/layouts/CenteredForm"; import Link from "next/link"; import { FormEvent, useState } from "react"; import { toast } from "react-hot-toast"; +import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useTranslation } from "next-i18next"; interface FormData { email: string; } export default function Forgot() { + const { t } = useTranslation(); const [submitLoader, setSubmitLoader] = useState(false); const [form, setForm] = useState({ @@ -43,7 +46,7 @@ export default function Forgot() { if (form.email !== "") { setSubmitLoader(true); - const load = toast.loading("Sending password recovery link..."); + const load = toast.loading(t("sending_password_link")); await submitRequest(); @@ -51,7 +54,7 @@ export default function Forgot() { setSubmitLoader(false); } else { - toast.error("Please fill out all the fields."); + toast.error(t("fill_all_fields")); } } @@ -60,7 +63,7 @@ export default function Forgot() {

    - {isEmailSent ? "Email Sent!" : "Forgot Password?"} + {isEmailSent ? t("email_sent") : t("forgot_password")}

    @@ -68,13 +71,10 @@ export default function Forgot() { {!isEmailSent ? ( <>
    -

    - Enter your email so we can send you a link to create a new - password. -

    +

    {t("password_email_prompt")}

    -

    Email

    +

    {t("email")}

    - Send Login Link + {t("send_reset_link")} ) : ( -

    - Check your email for a link to reset your password. If it doesn’t - appear within a few minutes, check your spam folder. -

    +

    {t("reset_email_sent_desc")}

    )}
    - Back to Login + {t("back_to_login")}
    @@ -113,3 +110,5 @@ export default function Forgot() { ); } + +export { getServerSideProps }; diff --git a/pages/links/index.tsx b/pages/links/index.tsx index e64ccb4..35c1488 100644 --- a/pages/links/index.tsx +++ b/pages/links/index.tsx @@ -1,24 +1,21 @@ import NoLinksFound from "@/components/NoLinksFound"; -import SortDropdown from "@/components/SortDropdown"; import useLinks from "@/hooks/useLinks"; import MainLayout from "@/layouts/MainLayout"; import useLinkStore from "@/store/links"; import React, { useEffect, useState } from "react"; import PageHeader from "@/components/PageHeader"; -import { Member, Sort, ViewMode } from "@/types/global"; -import ViewDropdown from "@/components/ViewDropdown"; +import { Sort, ViewMode } from "@/types/global"; import CardView from "@/components/LinkViews/Layouts/CardView"; import ListView from "@/components/LinkViews/Layouts/ListView"; -import useCollectivePermissions from "@/hooks/useCollectivePermissions"; -import toast from "react-hot-toast"; -import BulkDeleteLinksModal from "@/components/ModalContent/BulkDeleteLinksModal"; -import BulkEditLinksModal from "@/components/ModalContent/BulkEditLinksModal"; import { useRouter } from "next/router"; import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; +import LinkListOptions from "@/components/LinkListOptions"; +import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useTranslation } from "next-i18next"; export default function Links() { - const { links, selectedLinks, deleteLinksById, setSelectedLinks } = - useLinkStore(); + const { t } = useTranslation(); + const { links } = useLinkStore(); const [viewMode, setViewMode] = useState( localStorage.getItem("viewMode") || ViewMode.Card @@ -27,49 +24,14 @@ export default function Links() { const router = useRouter(); - const [bulkDeleteLinksModal, setBulkDeleteLinksModal] = useState(false); - const [bulkEditLinksModal, setBulkEditLinksModal] = useState(false); const [editMode, setEditMode] = useState(false); useEffect(() => { if (editMode) return setEditMode(false); }, [router]); - const collectivePermissions = useCollectivePermissions( - selectedLinks.map((link) => link.collectionId as number) - ); - useLinks({ sort: sortBy }); - const handleSelectAll = () => { - if (selectedLinks.length === links.length) { - setSelectedLinks([]); - } else { - setSelectedLinks(links.map((link) => link)); - } - }; - - const bulkDeleteLinks = async () => { - const load = toast.loading( - `Deleting ${selectedLinks.length} Link${ - selectedLinks.length > 1 ? "s" : "" - }...` - ); - - const response = await deleteLinksById( - selectedLinks.map((link) => link.id as number) - ); - - toast.dismiss(load); - - response.ok && - toast.success( - `Deleted ${selectedLinks.length} Link${ - selectedLinks.length > 1 ? "s" : "" - }!` - ); - }; - const linkView = { [ViewMode.Card]: CardView, [ViewMode.List]: ListView, @@ -82,113 +44,30 @@ export default function Links() { return (
    -
    + - -
    - {links.length > 0 && ( -
    { - setEditMode(!editMode); - setSelectedLinks([]); - }} - className={`btn btn-square btn-sm btn-ghost ${ - editMode - ? "bg-primary/20 hover:bg-primary/20" - : "hover:bg-neutral/20" - }`} - > - -
    - )} - - -
    -
    - - {editMode && links.length > 0 && ( -
    - {links.length > 0 && ( -
    - handleSelectAll()} - checked={ - selectedLinks.length === links.length && links.length > 0 - } - /> - {selectedLinks.length > 0 ? ( - - {selectedLinks.length}{" "} - {selectedLinks.length === 1 ? "link" : "links"} selected - - ) : ( - Nothing selected - )} -
    - )} -
    - - -
    -
    - )} + {links[0] ? ( ) : ( - + )}
    - {bulkDeleteLinksModal && ( - { - setBulkDeleteLinksModal(false); - }} - /> - )} - {bulkEditLinksModal && ( - { - setBulkEditLinksModal(false); - }} - /> - )}
    ); } + +export { getServerSideProps }; diff --git a/pages/links/pinned.tsx b/pages/links/pinned.tsx index a631573..60d75d8 100644 --- a/pages/links/pinned.tsx +++ b/pages/links/pinned.tsx @@ -1,23 +1,21 @@ -import SortDropdown from "@/components/SortDropdown"; import useLinks from "@/hooks/useLinks"; import MainLayout from "@/layouts/MainLayout"; import useLinkStore from "@/store/links"; import React, { useEffect, useState } from "react"; import PageHeader from "@/components/PageHeader"; import { Sort, ViewMode } from "@/types/global"; -import ViewDropdown from "@/components/ViewDropdown"; import CardView from "@/components/LinkViews/Layouts/CardView"; import ListView from "@/components/LinkViews/Layouts/ListView"; -import BulkDeleteLinksModal from "@/components/ModalContent/BulkDeleteLinksModal"; -import BulkEditLinksModal from "@/components/ModalContent/BulkEditLinksModal"; -import useCollectivePermissions from "@/hooks/useCollectivePermissions"; -import toast from "react-hot-toast"; import { useRouter } from "next/router"; import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; +import { useTranslation } from "next-i18next"; +import getServerSideProps from "@/lib/client/getServerSideProps"; +import LinkListOptions from "@/components/LinkListOptions"; export default function PinnedLinks() { - const { links, selectedLinks, deleteLinksById, setSelectedLinks } = - useLinkStore(); + const { t } = useTranslation(); + + const { links } = useLinkStore(); const [viewMode, setViewMode] = useState( localStorage.getItem("viewMode") || ViewMode.Card @@ -27,47 +25,12 @@ export default function PinnedLinks() { useLinks({ sort: sortBy, pinnedOnly: true }); const router = useRouter(); - const [bulkDeleteLinksModal, setBulkDeleteLinksModal] = useState(false); - const [bulkEditLinksModal, setBulkEditLinksModal] = useState(false); const [editMode, setEditMode] = useState(false); useEffect(() => { if (editMode) return setEditMode(false); }, [router]); - const collectivePermissions = useCollectivePermissions( - selectedLinks.map((link) => link.collectionId as number) - ); - - const handleSelectAll = () => { - if (selectedLinks.length === links.length) { - setSelectedLinks([]); - } else { - setSelectedLinks(links.map((link) => link)); - } - }; - - const bulkDeleteLinks = async () => { - const load = toast.loading( - `Deleting ${selectedLinks.length} Link${ - selectedLinks.length > 1 ? "s" : "" - }...` - ); - - const response = await deleteLinksById( - selectedLinks.map((link) => link.id as number) - ); - - toast.dismiss(load); - - response.ok && - toast.success( - `Deleted ${selectedLinks.length} Link${ - selectedLinks.length > 1 ? "s" : "" - }!` - ); - }; - const linkView = { [ViewMode.Card]: CardView, [ViewMode.List]: ListView, @@ -80,91 +43,21 @@ export default function PinnedLinks() { return (
    -
    + -
    - {!(links.length === 0) && ( -
    { - setEditMode(!editMode); - setSelectedLinks([]); - }} - className={`btn btn-square btn-sm btn-ghost ${ - editMode - ? "bg-primary/20 hover:bg-primary/20" - : "hover:bg-neutral/20" - }`} - > - -
    - )} - - -
    -
    - - {editMode && links.length > 0 && ( -
    - {links.length > 0 && ( -
    - handleSelectAll()} - checked={ - selectedLinks.length === links.length && links.length > 0 - } - /> - {selectedLinks.length > 0 ? ( - - {selectedLinks.length}{" "} - {selectedLinks.length === 1 ? "link" : "links"} selected - - ) : ( - Nothing selected - )} -
    - )} -
    - - -
    -
    - )} + {links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? ( @@ -182,30 +75,16 @@ export default function PinnedLinks() {

    - Pin Your Favorite Links Here! + {t("pin_favorite_links_here")}

    - You can Pin your favorite Links by clicking on the three dots on - each Link and clicking{" "} - Pin to Dashboard. + {t("pin_favorite_links_here_desc")}

    )}
    - {bulkDeleteLinksModal && ( - { - setBulkDeleteLinksModal(false); - }} - /> - )} - {bulkEditLinksModal && ( - { - setBulkEditLinksModal(false); - }} - /> - )} ); } + +export { getServerSideProps }; diff --git a/pages/login.tsx b/pages/login.tsx index e92cbde..aa77e1b 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -6,22 +6,27 @@ import Link from "next/link"; import React, { useState, FormEvent } from "react"; import { toast } from "react-hot-toast"; import { getLogins } from "./api/v1/logins"; -import { InferGetServerSidePropsType } from "next"; +import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import InstallApp from "@/components/InstallApp"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; +import { i18n } from "next-i18next.config"; +import { getToken } from "next-auth/jwt"; +import { prisma } from "@/lib/api/db"; +import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; interface FormData { username: string; password: string; } -export const getServerSideProps = () => { - const availableLogins = getLogins(); - return { props: { availableLogins } }; -}; - export default function Login({ availableLogins, }: InferGetServerSidePropsType) { + const { t } = useTranslation(); + + const router = useRouter(); + const [submitLoader, setSubmitLoader] = useState(false); const [form, setForm] = useState({ @@ -35,7 +40,7 @@ export default function Login({ if (form.username !== "" && form.password !== "") { setSubmitLoader(true); - const load = toast.loading("Authenticating..."); + const load = toast.loading(t("authenticating")); const res = await signIn("credentials", { username: form.username, @@ -48,17 +53,29 @@ export default function Login({ setSubmitLoader(false); if (!res?.ok) { - toast.error(res?.error || "Invalid credentials."); + toast.error(res?.error || t("invalid_credentials")); + + if (res?.error === "Email not verified.") { + await signIn("email", { + email: form.username, + callbackUrl: "/", + redirect: false, + }); + + router.push( + `/confirmation?email=${encodeURIComponent(form.username)}` + ); + } } } else { - toast.error("Please fill out all the fields."); + toast.error(t("fill_all_fields")); } } async function loginUserButton(method: string) { setSubmitLoader(true); - const load = toast.loading("Authenticating..."); + const load = toast.loading(t("authenticating")); const res = await signIn(method, {}); @@ -72,15 +89,14 @@ export default function Login({ return ( <>

    - Enter your credentials + {t("enter_credentials")}


    - Username {availableLogins.emailEnabled === "true" - ? " or Email" - : undefined} + ? t("username_or_email") + : t("username")}

    - Password + {t("password")}

    - Forgot Password? + {t("forgot_password")}
    )} @@ -124,11 +140,11 @@ export default function Login({ data-testid="submit-login-button" loading={submitLoader} > - Login + {t("login")} {availableLogins.buttonAuths.length > 0 ? ( -
    Or continue with
    +
    {t("or_continue_with")}
    ) : undefined} ); @@ -137,11 +153,9 @@ export default function Login({ function displayLoginExternalButton() { const Buttons: any = []; - availableLogins.buttonAuths.forEach((value, index) => { + availableLogins.buttonAuths.forEach((value: any, index: any) => { Buttons.push( - {index !== 0 ?
    Or
    : undefined} - loginUserButton(value.method)} @@ -165,13 +179,15 @@ export default function Login({ if (availableLogins.registrationDisabled !== "true") { return (
    -

    New here?

    +

    + {t("new_here")} +

    - Sign Up + {t("sign_up")}
    ); @@ -179,7 +195,7 @@ export default function Login({ } return ( - +
    ); } + +const getServerSideProps: GetServerSideProps = async (ctx) => { + const availableLogins = getLogins(); + + const acceptLanguageHeader = ctx.req.headers["accept-language"]; + const availableLanguages = i18n.locales; + + const token = await getToken({ req: ctx.req }); + + if (token) { + const user = await prisma.user.findUnique({ + where: { + id: token.id, + }, + }); + + if (user) { + return { + props: { + availableLogins, + ...(await serverSideTranslations(user.locale ?? "en", ["common"])), + }, + }; + } + } + + const acceptedLanguages = acceptLanguageHeader + ?.split(",") + .map((lang) => lang.split(";")[0]); + + let bestMatch = acceptedLanguages?.find((lang) => + availableLanguages.includes(lang) + ); + + if (!bestMatch) { + acceptedLanguages?.some((acceptedLang) => { + const partialMatch = availableLanguages.find((lang) => + lang.startsWith(acceptedLang) + ); + if (partialMatch) { + bestMatch = partialMatch; + return true; + } + return false; + }); + } + + return { + props: { + availableLogins, + ...(await serverSideTranslations(bestMatch ?? "en", ["common"])), + }, + }; +}; + +export { getServerSideProps }; diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 836d57a..8377518 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -7,7 +7,6 @@ import { } from "@/types/global"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; -import { motion, Variants } from "framer-motion"; import Head from "next/head"; import useLinks from "@/hooks/useLinks"; import useLinkStore from "@/store/links"; @@ -16,35 +15,25 @@ import ToggleDarkMode from "@/components/ToggleDarkMode"; import getPublicUserData from "@/lib/client/getPublicUserData"; import Image from "next/image"; import Link from "next/link"; -import FilterSearchDropdown from "@/components/FilterSearchDropdown"; -import SortDropdown from "@/components/SortDropdown"; import useLocalSettingsStore from "@/store/localSettings"; import SearchBar from "@/components/SearchBar"; import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal"; -import ViewDropdown from "@/components/ViewDropdown"; import CardView from "@/components/LinkViews/Layouts/CardView"; import ListView from "@/components/LinkViews/Layouts/ListView"; import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; - -const cardVariants: Variants = { - offscreen: { - y: 50, - opacity: 0, - }, - onscreen: { - y: 0, - opacity: 1, - transition: { - duration: 0.4, - }, - }, -}; +import { useTranslation } from "next-i18next"; +import getServerSideProps from "@/lib/client/getServerSideProps"; +import useCollectionStore from "@/store/collections"; +import LinkListOptions from "@/components/LinkListOptions"; export default function PublicCollections() { + const { t } = useTranslation(); const { links } = useLinkStore(); const { settings } = useLocalSettingsStore(); + const { collections } = useCollectionStore(); + const router = useRouter(); const [collectionOwner, setCollectionOwner] = useState({ @@ -85,7 +74,7 @@ export default function PublicCollections() { if (router.query.id) { getPublicCollectionData(Number(router.query.id), setCollection); } - }, []); + }, [collections]); useEffect(() => { const fetchOwner = async () => { @@ -147,7 +136,7 @@ export default function PublicCollections() { width={551} height={551} alt="Linkwarden" - title="Created with Linkwarden" + title={t("list_created_with_linkwarden")} className="h-8 w-fit mx-auto rounded" /> @@ -189,12 +178,22 @@ export default function PublicCollections() { ) : null}
    -

    - By {collectionOwner.name} - {collection.members.length > 0 - ? ` and ${collection.members.length} others` - : undefined} - . +

    + {collection.members.length > 0 && + 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, + }) + : t("by_author", { + author: collectionOwner.name, + })}

    @@ -205,22 +204,27 @@ export default function PublicCollections() {
    -
    + - -
    - - - - - -
    -
    + {links[0] ? ( ) : ( -

    This collection is empty...

    +

    {t("collection_is_empty")}

    )} {/*

    @@ -254,3 +258,5 @@ export default function PublicCollections() { <> ); } + +export { getServerSideProps }; diff --git a/pages/register.tsx b/pages/register.tsx index e0c5304..3e0dc07 100644 --- a/pages/register.tsx +++ b/pages/register.tsx @@ -7,7 +7,12 @@ import CenteredForm from "@/layouts/CenteredForm"; import TextInput from "@/components/TextInput"; import AccentSubmitButton from "@/components/ui/Button"; import { getLogins } from "./api/v1/logins"; -import { InferGetServerSidePropsType } from "next"; +import { GetServerSideProps, InferGetServerSidePropsType } from "next"; +import { getToken } from "next-auth/jwt"; +import { prisma } from "@/lib/api/db"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; +import { i18n } from "next-i18next.config"; +import { Trans, useTranslation } from "next-i18next"; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true"; @@ -19,14 +24,10 @@ type FormData = { passwordConfirmation: string; }; -export const getServerSideProps = () => { - const availableLogins = getLogins(); - return { props: { availableLogins } }; -}; - export default function Register({ availableLogins, }: InferGetServerSidePropsType) { + const { t } = useTranslation(); const [submitLoader, setSubmitLoader] = useState(false); const router = useRouter(); @@ -62,14 +63,14 @@ export default function Register({ if (checkFields()) { if (form.password !== form.passwordConfirmation) - return toast.error("Passwords do not match."); + return toast.error(t("passwords_mismatch")); else if (form.password.length < 8) - return toast.error("Passwords must be at least 8 characters."); + return toast.error(t("password_too_short")); const { passwordConfirmation, ...request } = form; setSubmitLoader(true); - const load = toast.loading("Creating Account..."); + const load = toast.loading(t("creating_account")); const response = await fetch("/api/v1/users", { body: JSON.stringify(request), @@ -97,12 +98,12 @@ export default function Register({ ); } else if (!emailEnabled) router.push("/login"); - toast.success("User Created!"); + toast.success(t("account_created")); } else { toast.error(data.response); } } else { - toast.error("Please fill out all the fields."); + toast.error(t("fill_all_fields")); } } } @@ -110,7 +111,7 @@ export default function Register({ async function loginUserButton(method: string) { setSubmitLoader(true); - const load = toast.loading("Authenticating..."); + const load = toast.loading(t("authenticating")); const res = await signIn(method, {}); @@ -121,11 +122,9 @@ export default function Register({ function displayLoginExternalButton() { const Buttons: any = []; - availableLogins.buttonAuths.forEach((value, index) => { + availableLogins.buttonAuths.forEach((value: any, index: any) => { Buttons.push( - {index !== 0 ?

    Or
    : undefined} - loginUserButton(value.method)} @@ -149,31 +148,30 @@ export default function Register({ {process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" ? (
    -

    - Registration is disabled for this instance, please contact the admin - in case of any issues. -

    +

    {t("registration_disabled")}

    ) : (

    - Enter your details + {t("enter_details")}

    -

    Display Name

    +

    + {t("display_name")} +

    -

    Username

    +

    + {t("username")} +

    -

    Email

    +

    {t("email")}

    -

    Password

    +

    + {t("password")} +

    - Confirm Password + {t("confirm_password")}

    {process.env.NEXT_PUBLIC_STRIPE ? ( -
    -

    - By signing up, you agree to our{" "} - - Terms of Service - {" "} - and{" "} - - Privacy Policy - - . -

    -

    - Need help?{" "} - - Get in touch - - . +

    +

    + + Terms of Services + , + + Privacy Policy + , + ]} + />

    ) : undefined} @@ -288,23 +281,37 @@ export default function Register({ size="full" data-testid="register-button" > - Sign Up + {t("sign_up")} {availableLogins.buttonAuths.length > 0 ? ( -
    Or continue with
    +
    {t("or_continue_with")}
    ) : undefined} {displayLoginExternalButton()} -
    -

    Already have an account?

    - - Login - +
    +
    +

    {t("already_registered")}

    + + {t("login")} + +
    + {process.env.NEXT_PUBLIC_STRIPE ? ( +
    +

    {t("need_help")}

    + + {t("get_in_touch")} + +
    + ) : undefined}
    @@ -312,3 +319,59 @@ export default function Register({ ); } + +const getServerSideProps: GetServerSideProps = async (ctx) => { + const availableLogins = getLogins(); + + const acceptLanguageHeader = ctx.req.headers["accept-language"]; + const availableLanguages = i18n.locales; + + const token = await getToken({ req: ctx.req }); + + if (token) { + const user = await prisma.user.findUnique({ + where: { + id: token.id, + }, + }); + + if (user) { + return { + props: { + availableLogins, + ...(await serverSideTranslations(user.locale ?? "en", ["common"])), + }, + }; + } + } + + const acceptedLanguages = acceptLanguageHeader + ?.split(",") + .map((lang) => lang.split(";")[0]); + + let bestMatch = acceptedLanguages?.find((lang) => + availableLanguages.includes(lang) + ); + + if (!bestMatch) { + acceptedLanguages?.some((acceptedLang) => { + const partialMatch = availableLanguages.find((lang) => + lang.startsWith(acceptedLang) + ); + if (partialMatch) { + bestMatch = partialMatch; + return true; + } + return false; + }); + } + + return { + props: { + availableLogins, + ...(await serverSideTranslations(bestMatch ?? "en", ["common"])), + }, + }; +}; + +export { getServerSideProps }; diff --git a/pages/search.tsx b/pages/search.tsx index 67d9893..d075cd8 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -1,25 +1,22 @@ -import FilterSearchDropdown from "@/components/FilterSearchDropdown"; -import SortDropdown from "@/components/SortDropdown"; import useLinks from "@/hooks/useLinks"; import MainLayout from "@/layouts/MainLayout"; import useLinkStore from "@/store/links"; import { Sort, ViewMode } from "@/types/global"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; -import ViewDropdown from "@/components/ViewDropdown"; import CardView from "@/components/LinkViews/Layouts/CardView"; import ListView from "@/components/LinkViews/Layouts/ListView"; import PageHeader from "@/components/PageHeader"; import { GridLoader } from "react-spinners"; -import useCollectivePermissions from "@/hooks/useCollectivePermissions"; -import toast from "react-hot-toast"; -import BulkDeleteLinksModal from "@/components/ModalContent/BulkDeleteLinksModal"; -import BulkEditLinksModal from "@/components/ModalContent/BulkEditLinksModal"; import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; +import LinkListOptions from "@/components/LinkListOptions"; +import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useTranslation } from "next-i18next"; export default function Search() { - const { links, selectedLinks, setSelectedLinks, deleteLinksById } = - useLinkStore(); + const { t } = useTranslation(); + + const { links } = useLinkStore(); const router = useRouter(); @@ -37,47 +34,12 @@ export default function Search() { const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); - const [bulkDeleteLinksModal, setBulkDeleteLinksModal] = useState(false); - const [bulkEditLinksModal, setBulkEditLinksModal] = useState(false); const [editMode, setEditMode] = useState(false); useEffect(() => { if (editMode) return setEditMode(false); }, [router]); - const collectivePermissions = useCollectivePermissions( - selectedLinks.map((link) => link.collectionId as number) - ); - - const handleSelectAll = () => { - if (selectedLinks.length === links.length) { - setSelectedLinks([]); - } else { - setSelectedLinks(links.map((link) => link)); - } - }; - - const bulkDeleteLinks = async () => { - const load = toast.loading( - `Deleting ${selectedLinks.length} Link${ - selectedLinks.length > 1 ? "s" : "" - }...` - ); - - const response = await deleteLinksById( - selectedLinks.map((link) => link.id as number) - ); - - toast.dismiss(load); - - response.ok && - toast.success( - `Deleted ${selectedLinks.length} Link${ - selectedLinks.length > 1 ? "s" : "" - }!` - ); - }; - const { isLoading } = useLinks({ sort: sortBy, searchQueryString: decodeURIComponent(router.query.q as string), @@ -88,10 +50,6 @@ export default function Search() { searchByTags: searchFilter.tags, }); - useEffect(() => { - console.log("isLoading", isLoading); - }, [isLoading]); - const linkView = { [ViewMode.Card]: CardView, [ViewMode.List]: ListView, @@ -104,102 +62,22 @@ export default function Search() { return (
    -
    + - -
    -
    - {links.length > 0 && ( -
    { - setEditMode(!editMode); - setSelectedLinks([]); - }} - className={`btn btn-square btn-sm btn-ghost ${ - editMode - ? "bg-primary/20 hover:bg-primary/20" - : "hover:bg-neutral/20" - }`} - > - -
    - )} - - - -
    -
    -
    - - {editMode && links.length > 0 && ( -
    - {links.length > 0 && ( -
    - handleSelectAll()} - checked={ - selectedLinks.length === links.length && links.length > 0 - } - /> - {selectedLinks.length > 0 ? ( - - {selectedLinks.length}{" "} - {selectedLinks.length === 1 ? "link" : "links"} selected - - ) : ( - Nothing selected - )} -
    - )} -
    - - -
    -
    - )} + {!isLoading && !links[0] ? ( -

    - Nothing found.{" "} - - ¯\_(ツ)_/¯ - -

    +

    {t("nothing_found")}

    ) : links[0] ? ( - {bulkDeleteLinksModal && ( - { - setBulkDeleteLinksModal(false); - }} - /> - )} - {bulkEditLinksModal && ( - { - setBulkEditLinksModal(false); - }} - /> - )} ); } + +export { getServerSideProps }; diff --git a/pages/settings/access-tokens.tsx b/pages/settings/access-tokens.tsx index 1204ce8..ef66f6d 100644 --- a/pages/settings/access-tokens.tsx +++ b/pages/settings/access-tokens.tsx @@ -4,11 +4,14 @@ import NewTokenModal from "@/components/ModalContent/NewTokenModal"; import RevokeTokenModal from "@/components/ModalContent/RevokeTokenModal"; import { AccessToken } from "@prisma/client"; import useTokenStore from "@/store/tokens"; +import { useTranslation } from "next-i18next"; +import getServerSideProps from "@/lib/client/getServerSideProps"; export default function AccessTokens() { const [newTokenModal, setNewTokenModal] = useState(false); const [revokeTokenModal, setRevokeTokenModal] = useState(false); const [selectedToken, setSelectedToken] = useState(null); + const { t } = useTranslation(); const openRevokeModal = (token: AccessToken) => { setSelectedToken(token); @@ -27,15 +30,14 @@ export default function AccessTokens() { return ( -

    Access Tokens

    +

    + {t("access_tokens")} +

    -

    - Access Tokens can be used to access Linkwarden from other apps and - services without giving away your Username and Password. -

    +

    {t("access_tokens_description")}

    {tokens.length > 0 ? ( @@ -51,13 +53,12 @@ export default function AccessTokens() {
    - {/* head */} - - - + + + @@ -105,3 +106,5 @@ export default function AccessTokens() { ); } + +export { getServerSideProps }; diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index 2d0c2b3..cb46442 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -15,17 +15,16 @@ import { dropdownTriggerer } from "@/lib/client/utils"; import EmailChangeVerificationModal from "@/components/ModalContent/EmailChangeVerificationModal"; import Button from "@/components/ui/Button"; import { i18n } from "next-i18next.config"; +import { useTranslation } from "next-i18next"; +import getServerSideProps from "@/lib/client/getServerSideProps"; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; export default function Account() { const [emailChangeVerificationModal, setEmailChangeVerificationModal] = useState(false); - const [submitLoader, setSubmitLoader] = useState(false); - const { account, updateAccount } = useAccountStore(); - const [user, setUser] = useState( !objectIsEmpty(account) ? account @@ -45,6 +44,8 @@ export default function Account() { } as unknown as AccountSettings) ); + const { t } = useTranslation(); + function objectIsEmpty(obj: object) { return Object.keys(obj).length === 0; } @@ -68,17 +69,16 @@ export default function Account() { }; reader.readAsDataURL(resizedFile); } else { - toast.error("Please select a PNG or JPEG file thats less than 1MB."); + toast.error(t("image_upload_size_error")); } } else { - toast.error("Invalid file format."); + toast.error(t("image_upload_format_error")); } }; const submit = async (password?: string) => { setSubmitLoader(true); - - const load = toast.loading("Applying..."); + const load = toast.loading(t("applying_settings")); const response = await updateAccount({ ...user, @@ -91,56 +91,44 @@ export default function Account() { if (response.ok) { const emailChanged = account.email !== user.email; + toast.success(t("settings_applied")); if (emailChanged) { - toast.success("Settings Applied!"); - toast.success( - "Email change request sent. Please verify the new email address." - ); + toast.success(t("email_change_request")); setEmailChangeVerificationModal(false); - } else toast.success("Settings Applied!"); + } } else toast.error(response.data as string); setSubmitLoader(false); }; const importBookmarks = async (e: any, format: MigrationFormat) => { setSubmitLoader(true); - const file: File = e.target.files[0]; - if (file) { var reader = new FileReader(); reader.readAsText(file, "UTF-8"); reader.onload = async function (e) { - const load = toast.loading("Importing..."); - + const load = toast.loading(t("importing_bookmarks")); const request: string = e.target?.result as string; - - const body: MigrationRequest = { - format, - data: request, - }; - + const body: MigrationRequest = { format, data: request }; const response = await fetch("/api/v1/migration", { method: "POST", body: JSON.stringify(body), }); - const data = await response.json(); - toast.dismiss(load); - if (response.ok) { - toast.success("Imported the Bookmarks! Reloading the page..."); + toast.success(t("import_success")); setTimeout(() => { location.reload(); }, 2000); - } else toast.error(data.response as string); + } else { + toast.error(data.response as string); + } }; reader.onerror = function (e) { console.log("Error:", e); }; } - setSubmitLoader(false); }; @@ -158,16 +146,14 @@ export default function Account() { }, [whitelistedUsersTextbox]); const stringToArray = (str: string) => { - const stringWithoutSpaces = str?.replace(/\s+/g, ""); - - const wordsArray = stringWithoutSpaces?.split(","); - - return wordsArray; + return str?.replace(/\s+/g, "").split(","); }; return ( -

    Account Settings

    +

    + {t("accountSettings")} +

    @@ -175,7 +161,7 @@ export default function Account() {
    -

    Display Name

    +

    {t("display_name")}

    -

    Username

    +

    {t("username")}

    setUser({ ...user, username: e.target.value })} />
    - {emailEnabled ? (
    -

    Email

    +

    {t("email")}

    ) : undefined} -
    -

    Language

    +

    {t("language")}

    -

    Profile Photo

    +

    {t("profile_photo")}

    - Edit + {t("edit")}
    )} @@ -284,25 +269,22 @@ export default function Account() {
    setUser({ ...user, isPrivate: !user.isPrivate })} /> -

    - This will limit who can find and add you to new Collections. -

    +

    {t("profile_privacy_info")}

    {user.isPrivate && (
    -

    Whitelisted Users

    +

    {t("whitelisted_users")}

    - Please provide the Username of the users you wish to grant - visibility to your profile. Separated by comma. + {t("whitelisted_users_info")}

    NameCreatedExpires{t("name")}{t("created")}{t("expires")}