diff --git a/.vscode/settings.json b/.vscode/settings.json index 0967ef4..b446483 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,6 @@ -{} +{ + "tailwindCSS.experimental.classRegex": [ + ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], + ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] + ] +} diff --git a/components/AccentSubmitButton.tsx b/components/AccentSubmitButton.tsx deleted file mode 100644 index 930c1a6..0000000 --- a/components/AccentSubmitButton.tsx +++ /dev/null @@ -1,32 +0,0 @@ -type Props = { - onClick?: Function; - label: string; - loading?: boolean; - className?: string; - type?: "button" | "submit" | "reset" | undefined; - "data-testid"?: string; -}; - -export default function AccentSubmitButton({ - onClick, - label, - loading, - className, - type, - "data-testid": dataTestId, -}: Props) { - return ( - - ); -} diff --git a/components/Announcement.tsx b/components/Announcement.tsx new file mode 100644 index 0000000..4696179 --- /dev/null +++ b/components/Announcement.tsx @@ -0,0 +1,35 @@ +import Link from "next/link"; +import React, { MouseEventHandler } from "react"; + +type Props = { + toggleAnnouncementBar: MouseEventHandler; +}; + +export default function AnnouncementBar({ toggleAnnouncementBar }: Props) { + const announcementId = localStorage.getItem("announcementId"); + + return ( +
+
+ +

+ See what's new in{" "} + + Linkwarden {announcementId} + + ! +

+ +
+
+ ); +} diff --git a/components/AnnouncementBar.tsx b/components/AnnouncementBar.tsx deleted file mode 100644 index 57b7e92..0000000 --- a/components/AnnouncementBar.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import Link from "next/link"; -import React, { MouseEventHandler } from "react"; - -type Props = { - toggleAnnouncementBar: MouseEventHandler; -}; - -export default function AnnouncementBar({ toggleAnnouncementBar }: Props) { - return ( -
-
-
- 🎉️ See what's new in{" "} - - Linkwarden v2.5 - - ! 🥳️ -
- - -
-
- ); -} diff --git a/components/InstallApp.tsx b/components/InstallApp.tsx new file mode 100644 index 0000000..b8bcf84 --- /dev/null +++ b/components/InstallApp.tsx @@ -0,0 +1,51 @@ +import { isPWA } from "@/lib/client/utils"; +import React, { useState } from "react"; + +type Props = {}; + +const InstallApp = (props: Props) => { + const [isOpen, setIsOpen] = useState(true); + + return isOpen && !isPWA() ? ( +
+
+ + + + + +

+ Install Linkwarden to your home screen for a faster access and + enhanced experience.{" "} + + Learn more + +

+ +
+
+ ) : ( + <> + ); +}; + +export default InstallApp; diff --git a/components/Navbar.tsx b/components/Navbar.tsx index f826f9d..344793b 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -4,7 +4,7 @@ import Sidebar from "@/components/Sidebar"; import { useRouter } from "next/router"; import SearchBar from "@/components/SearchBar"; import useWindowDimensions from "@/hooks/useWindowDimensions"; -import ToggleDarkMode from "./ToggleDarkMode"; +import ToggleDarkMode from "./ui/ToggleDarkMode"; import NewLinkModal from "./ModalContent/NewLinkModal"; import NewCollectionModal from "./ModalContent/NewCollectionModal"; import UploadFileModal from "./ModalContent/UploadFileModal"; diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx new file mode 100644 index 0000000..2494abb --- /dev/null +++ b/components/ui/Button.tsx @@ -0,0 +1,60 @@ +import { cn } from "@/lib/client/utils"; + +import { cva, type VariantProps } from "class-variance-authority"; + +const buttonVariants = cva( + "select-none relative duration-200 rounded-lg text-center w-fit flex justify-center items-center gap-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + intent: { + accent: + "bg-accent text-white hover:bg-accent/80 border border-violet-400", + primary: "bg-primary text-primary-content hover:bg-primary/80", + secondary: + "bg-neutral-content text-secondary-foreground hover:bg-neutral-content/80 border border-neutral/30", + destructive: "bg-error text-white hover:bg-error/80", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-content", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + small: "h-9 px-3", + medium: "h-10 px-4 py-2", + full: "px-4 py-2 w-full", + icon: "h-10 w-10", + }, + loading: { + true: "cursor-wait", + }, + }, + defaultVariants: { + intent: "primary", + size: "medium", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps {} + +const Button: React.FC = ({ + className, + intent, + size, + children, + disabled, + loading = false, + ...props +}) => ( + +); + +export default Button; diff --git a/components/ToggleDarkMode.tsx b/components/ui/ToggleDarkMode.tsx similarity index 100% rename from components/ToggleDarkMode.tsx rename to components/ui/ToggleDarkMode.tsx diff --git a/layouts/AuthRedirect.tsx b/layouts/AuthRedirect.tsx index 7f88b45..18c9a8a 100644 --- a/layouts/AuthRedirect.tsx +++ b/layouts/AuthRedirect.tsx @@ -9,7 +9,6 @@ interface Props { children: ReactNode; } -const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true"; const stripeEnabled = process.env.NEXT_PUBLIC_STRIPE === "true"; export default function AuthRedirect({ children }: Props) { @@ -26,6 +25,8 @@ export default function AuthRedirect({ children }: Props) { const isPublicPage = router.pathname.startsWith("/public"); const hasInactiveSubscription = account.id && !account.subscription?.active && stripeEnabled; + + // There are better ways of doing this... but this one works for now const routes = [ { path: "/login", isProtected: false }, { path: "/register", isProtected: false }, @@ -53,9 +54,7 @@ export default function AuthRedirect({ children }: Props) { redirectTo("/dashboard"); } else if ( isUnauthenticated && - !routes.some( - (e) => router.pathname.startsWith(e.path) && !e.isProtected - ) + routes.some((e) => router.pathname.startsWith(e.path) && e.isProtected) ) { redirectTo("/login"); } else { diff --git a/layouts/MainLayout.tsx b/layouts/MainLayout.tsx index a1178a5..2c606e5 100644 --- a/layouts/MainLayout.tsx +++ b/layouts/MainLayout.tsx @@ -1,5 +1,5 @@ import Navbar from "@/components/Navbar"; -import AnnouncementBar from "@/components/AnnouncementBar"; +import Announcement from "@/components/Announcement"; import Sidebar from "@/components/Sidebar"; import { ReactNode, useEffect, useState } from "react"; import getLatestVersion from "@/lib/client/getLatestVersion"; @@ -33,27 +33,20 @@ export default function MainLayout({ children }: Props) { }; return ( - <> +
{showAnnouncement ? ( - + ) : undefined} - -
-
- -
- -
- - {children} -
+
+
- + +
+ + {children} +
+
); } diff --git a/lib/api/controllers/users/userId/updateUserById.ts b/lib/api/controllers/users/userId/updateUserById.ts index 2b03528..2c2d81f 100644 --- a/lib/api/controllers/users/userId/updateUserById.ts +++ b/lib/api/controllers/users/userId/updateUserById.ts @@ -129,7 +129,8 @@ export default async function updateUserById( // Verify password if (!user.password) { return { - response: "User has no password.", + response: + "User has no password. Please reset your password from the forgot password page.", status: 400, }; } diff --git a/lib/client/utils.ts b/lib/client/utils.ts index 7d139c5..e067933 100644 --- a/lib/client/utils.ts +++ b/lib/client/utils.ts @@ -18,3 +18,7 @@ export function dropdownTriggerer(e: any) { }, 0); } } + +import clsx, { ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; +export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes)); diff --git a/package.json b/package.json index 13d0fd9..8222cb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "linkwarden", - "version": "v2.5.4", + "version": "v2.6.0", "main": "index.js", "repository": "https://github.com/linkwarden/linkwarden.git", "author": "Daniel31X13 ", @@ -36,6 +36,8 @@ "axios": "^1.5.1", "bcrypt": "^5.1.0", "bootstrap-icons": "^1.11.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", "colorthief": "^2.4.0", "concurrently": "^8.2.2", "crypto-js": "^4.2.0", @@ -67,6 +69,7 @@ "react-spinners": "^0.13.8", "socks-proxy-agent": "^8.0.2", "stripe": "^12.13.0", + "tailwind-merge": "^2.3.0", "vaul": "^0.8.8", "zustand": "^4.3.8" }, diff --git a/pages/api/v1/auth/[...nextauth].ts b/pages/api/v1/auth/[...nextauth].ts index a67bdf1..91fe7a7 100644 --- a/pages/api/v1/auth/[...nextauth].ts +++ b/pages/api/v1/auth/[...nextauth].ts @@ -608,6 +608,9 @@ if (process.env.NEXT_PUBLIC_GOOGLE_ENABLED === "true") { GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + httpOptions: { + timeout: 10000, + }, }) ); @@ -1162,20 +1165,6 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) { }, callbacks: { async signIn({ user, account, profile, email, credentials }) { - // console.log( - // "User sign in attempt...", - // "User", - // user, - // "Account", - // account, - // "Profile", - // profile, - // "Email", - // email, - // "Credentials", - // credentials - // ); - if (account?.provider !== "credentials") { // registration via SSO can be separately disabled const existingUser = await prisma.account.findFirst({ diff --git a/pages/auth/reset-password.tsx b/pages/auth/reset-password.tsx index 138d6ff..44d6abf 100644 --- a/pages/auth/reset-password.tsx +++ b/pages/auth/reset-password.tsx @@ -1,4 +1,4 @@ -import AccentSubmitButton from "@/components/AccentSubmitButton"; +import AccentSubmitButton from "@/components/ui/Button"; import TextInput from "@/components/TextInput"; import CenteredForm from "@/layouts/CenteredForm"; import Link from "next/link"; @@ -96,10 +96,13 @@ export default function ResetPassword() { + > + Update Password + ) : ( <> diff --git a/pages/forgot.tsx b/pages/forgot.tsx index 70dc8af..ff2ce9c 100644 --- a/pages/forgot.tsx +++ b/pages/forgot.tsx @@ -1,4 +1,4 @@ -import AccentSubmitButton from "@/components/AccentSubmitButton"; +import AccentSubmitButton from "@/components/ui/Button"; import TextInput from "@/components/TextInput"; import CenteredForm from "@/layouts/CenteredForm"; import Link from "next/link"; @@ -88,10 +88,13 @@ export default function Forgot() { + > + Send Login Link + ) : (

diff --git a/pages/login.tsx b/pages/login.tsx index 90d26d1..e92cbde 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -1,4 +1,4 @@ -import AccentSubmitButton from "@/components/AccentSubmitButton"; +import AccentSubmitButton from "@/components/ui/Button"; import TextInput from "@/components/TextInput"; import CenteredForm from "@/layouts/CenteredForm"; import { signIn } from "next-auth/react"; @@ -7,6 +7,7 @@ import React, { useState, FormEvent } from "react"; import { toast } from "react-hot-toast"; import { getLogins } from "./api/v1/logins"; import { InferGetServerSidePropsType } from "next"; +import InstallApp from "@/components/InstallApp"; interface FormData { username: string; @@ -118,33 +119,42 @@ export default function Login({

+ > + Login + {availableLogins.buttonAuths.length > 0 ? ( -
OR
+
Or continue with
) : undefined} ); } } + function displayLoginExternalButton() { const Buttons: any = []; availableLogins.buttonAuths.forEach((value, index) => { Buttons.push( - {index !== 0 ?
OR
: undefined} + {index !== 0 ?
Or
: undefined} loginUserButton(value.method)} - label={`Sign in with ${value.name}`} - className=" w-full text-center" + size="full" + intent="secondary" loading={submitLoader} - /> + > + {value.name.toLowerCase() === "google" || + value.name.toLowerCase() === "apple" ? ( + + ) : undefined} + {value.name} +
); }); @@ -178,15 +188,9 @@ export default function Login({ {displayLoginCredential()} {displayLoginExternalButton()} {displayRegistration()} - - You can install Linkwarden onto your device - + ); } diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 05574a7..289c4c4 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -12,7 +12,7 @@ import Head from "next/head"; import useLinks from "@/hooks/useLinks"; import useLinkStore from "@/store/links"; import ProfilePhoto from "@/components/ProfilePhoto"; -import ToggleDarkMode from "@/components/ToggleDarkMode"; +import ToggleDarkMode from "@/components/ui/ToggleDarkMode"; import getPublicUserData from "@/lib/client/getPublicUserData"; import Image from "next/image"; import Link from "next/link"; @@ -118,8 +118,9 @@ export default function PublicCollections() {
{collection ? ( diff --git a/pages/register.tsx b/pages/register.tsx index c3d63b1..e0c5304 100644 --- a/pages/register.tsx +++ b/pages/register.tsx @@ -1,11 +1,13 @@ import Link from "next/link"; -import { useState, FormEvent } from "react"; +import React, { useState, FormEvent } from "react"; import { toast } from "react-hot-toast"; import { signIn } from "next-auth/react"; import { useRouter } from "next/router"; import CenteredForm from "@/layouts/CenteredForm"; import TextInput from "@/components/TextInput"; -import AccentSubmitButton from "@/components/AccentSubmitButton"; +import AccentSubmitButton from "@/components/ui/Button"; +import { getLogins } from "./api/v1/logins"; +import { InferGetServerSidePropsType } from "next"; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true"; @@ -17,7 +19,14 @@ type FormData = { passwordConfirmation: string; }; -export default function Register() { +export const getServerSideProps = () => { + const availableLogins = getLogins(); + return { props: { availableLogins } }; +}; + +export default function Register({ + availableLogins, +}: InferGetServerSidePropsType) { const [submitLoader, setSubmitLoader] = useState(false); const router = useRouter(); @@ -98,6 +107,44 @@ export default function Register() { } } + async function loginUserButton(method: string) { + setSubmitLoader(true); + + const load = toast.loading("Authenticating..."); + + const res = await signIn(method, {}); + + toast.dismiss(load); + + setSubmitLoader(false); + } + + function displayLoginExternalButton() { + const Buttons: any = []; + availableLogins.buttonAuths.forEach((value, index) => { + Buttons.push( + + {index !== 0 ?
Or
: undefined} + + loginUserButton(value.method)} + size="full" + intent="secondary" + loading={submitLoader} + > + {value.name.toLowerCase() === "google" || + value.name.toLowerCase() === "apple" ? ( + + ) : undefined} + {value.name} + +
+ ); + }); + return Buttons; + } + return ( + > + Sign Up + + + {availableLogins.buttonAuths.length > 0 ? ( +
Or continue with
+ ) : undefined} + + {displayLoginExternalButton()}

Already have an account?

Yearly

-
+
25% Off
@@ -98,11 +98,13 @@ export default function Subscribe() { + > + Complete Subscription! +
signOut()} diff --git a/styles/globals.css b/styles/globals.css index 256106b..1f97b6e 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -255,32 +255,6 @@ border-radius: 8px; } -.rainbow { - background: linear-gradient( - 45deg, - #ff00004b, - #ff99004b, - #33cc334b, - #0099cc4b, - #9900cc4b, - #ff33cc4b - ); - background-size: 400% 400%; - animation: rainbow 30s linear infinite; -} - -@keyframes rainbow { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - .custom-file-input::file-selector-button { cursor: pointer; } diff --git a/yarn.lock b/yarn.lock index d6ebd45..fea7154 100644 --- a/yarn.lock +++ b/yarn.lock @@ -666,6 +666,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.24.1": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c" + integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/types@^7.18.6": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.0.tgz#1da00d89c2f18b226c9207d96edbeb79316a1819" @@ -2551,6 +2558,13 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +class-variance-authority@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.0.tgz#1c3134d634d80271b1837452b06d821915954522" + integrity sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A== + dependencies: + clsx "2.0.0" + client-only@0.0.1, client-only@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" @@ -2565,6 +2579,16 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +clsx@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" + integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== + +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -5751,6 +5775,13 @@ synckit@^0.8.4: "@pkgr/utils" "^2.3.1" tslib "^2.5.0" +tailwind-merge@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.3.0.tgz#27d2134fd00a1f77eca22bcaafdd67055917d286" + integrity sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA== + dependencies: + "@babel/runtime" "^7.24.1" + tailwindcss@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"