refactor code to improve readability and maintainability + redesigned announcement bar

This commit is contained in:
daniel31x13 2024-05-22 20:56:56 -04:00
parent 811628a952
commit a498f3a10d
22 changed files with 319 additions and 171 deletions

View File

@ -1 +1,6 @@
{}
{
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}

View File

@ -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 (
<button
type={type ? type : undefined}
className={`border primary-btn-gradient select-none duration-200 bg-black border-[oklch(var(--p))] hover:border-[#0070b5] rounded-lg text-center px-4 py-2 text-white active:scale-95 tracking-wider w-fit flex justify-center items-center gap-2 ${
className || ""
}`}
data-testid={dataTestId}
onClick={() => {
if (loading !== undefined && !loading && onClick) onClick();
}}
>
<p className="font-bold">{label}</p>
</button>
);
}

View File

@ -0,0 +1,35 @@
import Link from "next/link";
import React, { MouseEventHandler } from "react";
type Props = {
toggleAnnouncementBar: MouseEventHandler<HTMLButtonElement>;
};
export default function AnnouncementBar({ toggleAnnouncementBar }: Props) {
const announcementId = localStorage.getItem("announcementId");
return (
<div className="fixed left-0 right-0 bottom-20 sm:bottom-10 w-full p-5 z-20">
<div className="mx-auto w-full p-2 flex justify-between gap-2 items-center border border-primary shadow-xl rounded-xl bg-base-300 backdrop-blur-sm bg-opacity-80 max-w-md">
<i className="bi-stars text-2xl text-yellow-600 dark:text-yellow-500"></i>
<p className="w-4/5 text-center text-sm sm:text-base">
See what&apos;s new in{" "}
<Link
href={`https://blog.linkwarden.app/releases/${announcementId}`}
target="_blank"
className="underline"
>
Linkwarden {announcementId}
</Link>
!
</p>
<button
onClick={toggleAnnouncementBar}
className="btn btn-ghost btn-square btn-sm"
>
<i className="bi-x text-xl"></i>
</button>
</div>
</div>
);
}

View File

@ -1,33 +0,0 @@
import Link from "next/link";
import React, { MouseEventHandler } from "react";
type Props = {
toggleAnnouncementBar: MouseEventHandler<HTMLButtonElement>;
};
export default function AnnouncementBar({ toggleAnnouncementBar }: Props) {
return (
<div className="fixed w-full z-20 bg-base-200">
<div className="w-full h-10 rainbow flex items-center justify-center">
<div className="w-fit font-semibold">
🎉 See what&apos;s new in{" "}
<Link
href="https://blog.linkwarden.app/releases/v2.5"
target="_blank"
className="underline hover:opacity-50 duration-100"
>
Linkwarden v2.5
</Link>
! 🥳
</div>
<button
className="fixed right-3 hover:opacity-50 duration-100"
onClick={toggleAnnouncementBar}
>
<i className="bi-x text-neutral text-2xl"></i>
</button>
</div>
</div>
);
}

51
components/InstallApp.tsx Normal file
View File

@ -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() ? (
<div className="absolute left-0 right-0 bottom-10 w-full p-5">
<div className="mx-auto w-fit p-2 flex justify-between gap-2 items-center border border-neutral-content rounded-xl bg-base-300 backdrop-blur-md bg-opacity-80 max-w-md">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-8 h-8"
viewBox="0 0 50 50"
>
<path
fill="currentColor"
d="M30.3 13.7L25 8.4l-5.3 5.3l-1.4-1.4L25 5.6l6.7 6.7z"
/>
<path fill="currentColor" d="M24 7h2v21h-2z" />
<path
fill="currentColor"
d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3"
/>
</svg>
<p className="w-4/5 text-[0.92rem]">
Install Linkwarden to your home screen for a faster access and
enhanced experience.{" "}
<a
className="underline"
target="_blank"
href="https://docs.linkwarden.app/getting-started/pwa-installation"
>
Learn more
</a>
</p>
<button
onClick={() => setIsOpen(false)}
className="btn btn-ghost btn-square btn-sm"
>
<i className="bi-x text-xl"></i>
</button>
</div>
</div>
) : (
<></>
);
};
export default InstallApp;

View File

@ -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";

60
components/ui/Button.tsx Normal file
View File

@ -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<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button: React.FC<ButtonProps> = ({
className,
intent,
size,
children,
disabled,
loading = false,
...props
}) => (
<button
className={cn(buttonVariants({ intent, size, className }))}
disabled={loading || disabled}
{...props}
>
{children}
</button>
);
export default Button;

View File

@ -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 {

View File

@ -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 ? (
<AnnouncementBar toggleAnnouncementBar={toggleAnnouncementBar} />
) : undefined}
<div className="flex" data-testid="dashboard-wrapper">
{showAnnouncement ? (
<Announcement toggleAnnouncementBar={toggleAnnouncementBar} />
) : undefined}
<div className="hidden lg:block">
<Sidebar
className={`fixed ${showAnnouncement ? "top-10" : "top-0"}`}
/>
<Sidebar className={`fixed top-0`} />
</div>
<div
className={`w-full sm:pb-0 pb-20 flex flex-col min-h-${
showAnnouncement ? "full" : "screen"
} lg:ml-80 ${showAnnouncement ? "mt-10" : ""}`}
className={`w-full sm:pb-0 pb-20 flex flex-col min-h-screen lg:ml-80`}
>
<Navbar />
{children}
</div>
</div>
</>
);
}

View File

@ -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,
};
}

View File

@ -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));

View File

@ -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 <daniel31x13@gmail.com>",
@ -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"
},

View File

@ -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({

View File

@ -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() {
<AccentSubmitButton
type="submit"
label="Update Password"
className="mt-2 w-full"
intent="accent"
className="mt-2"
size="full"
loading={submitLoader}
/>
>
Update Password
</AccentSubmitButton>
</>
) : (
<>

View File

@ -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() {
<AccentSubmitButton
type="submit"
label="Send Login Link"
className="mt-2 w-full"
intent="accent"
className="mt-2"
size="full"
loading={submitLoader}
/>
>
Send Login Link
</AccentSubmitButton>
</>
) : (
<p>

View File

@ -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({
</div>
<AccentSubmitButton
type="submit"
label="Login"
className=" w-full text-center"
size="full"
intent="accent"
data-testid="submit-login-button"
loading={submitLoader}
/>
>
Login
</AccentSubmitButton>
{availableLogins.buttonAuths.length > 0 ? (
<div className="divider my-1">OR</div>
<div className="divider my-1">Or continue with</div>
) : undefined}
</>
);
}
}
function displayLoginExternalButton() {
const Buttons: any = [];
availableLogins.buttonAuths.forEach((value, index) => {
Buttons.push(
<React.Fragment key={index}>
{index !== 0 ? <div className="divider my-1">OR</div> : undefined}
{index !== 0 ? <div className="divider my-1">Or</div> : undefined}
<AccentSubmitButton
type="button"
onClick={() => 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" ? (
<i className={"bi-" + value.name.toLowerCase()}></i>
) : undefined}
{value.name}
</AccentSubmitButton>
</React.Fragment>
);
});
@ -178,15 +188,9 @@ export default function Login({
{displayLoginCredential()}
{displayLoginExternalButton()}
{displayRegistration()}
<Link
href="https://docs.linkwarden.app/getting-started/pwa-installation"
className="underline text-center"
target="_blank"
>
You can install Linkwarden onto your device
</Link>
</div>
</form>
<InstallApp />
</CenteredForm>
);
}

View File

@ -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,7 +118,8 @@ export default function PublicCollections() {
<div
className="h-96"
style={{
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${settings.theme === "dark" ? "#262626" : "#f3f4f6"
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
settings.theme === "dark" ? "#262626" : "#f3f4f6"
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}}
>

View File

@ -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<typeof getServerSideProps>) {
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(
<React.Fragment key={index}>
{index !== 0 ? <div className="divider my-1">Or</div> : undefined}
<AccentSubmitButton
type="button"
onClick={() => loginUserButton(value.method)}
size="full"
intent="secondary"
loading={submitLoader}
>
{value.name.toLowerCase() === "google" ||
value.name.toLowerCase() === "apple" ? (
<i className={"bi-" + value.name.toLowerCase()}></i>
) : undefined}
{value.name}
</AccentSubmitButton>
</React.Fragment>
);
});
return Buttons;
}
return (
<CenteredForm
text={
@ -236,11 +283,19 @@ export default function Register() {
<AccentSubmitButton
type="submit"
label="Sign Up"
className="w-full"
loading={submitLoader}
intent="accent"
size="full"
data-testid="register-button"
/>
>
Sign Up
</AccentSubmitButton>
{availableLogins.buttonAuths.length > 0 ? (
<div className="divider my-1">Or continue with</div>
) : undefined}
{displayLoginExternalButton()}
<div className="flex items-baseline gap-1 justify-center">
<p className="w-fit text-neutral">Already have an account?</p>
<Link

View File

@ -4,7 +4,7 @@ import { toast } from "react-hot-toast";
import { useRouter } from "next/router";
import CenteredForm from "@/layouts/CenteredForm";
import { Plan } from "@/types/global";
import AccentSubmitButton from "@/components/AccentSubmitButton";
import AccentSubmitButton from "@/components/ui/Button";
export default function Subscribe() {
const [submitLoader, setSubmitLoader] = useState(false);
@ -70,7 +70,7 @@ export default function Subscribe() {
>
<p>Yearly</p>
</button>
<div className="absolute -top-3 -right-4 px-1 bg-red-500 text-sm text-white rounded-md rotate-[22deg]">
<div className="absolute -top-3 -right-4 px-1 bg-red-600 text-sm text-white rounded-md rotate-[22deg]">
25% Off
</div>
</div>
@ -98,11 +98,13 @@ export default function Subscribe() {
<AccentSubmitButton
type="button"
label="Complete Subscription!"
className="w-full"
intent="accent"
size="full"
onClick={submit}
loading={submitLoader}
/>
>
Complete Subscription!
</AccentSubmitButton>
<div
onClick={() => signOut()}

View File

@ -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;
}

View File

@ -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"