refactor code to improve readability and maintainability + redesigned announcement bar

This commit is contained in:
daniel31x13 2024-05-24 17:12:47 -04:00
parent a498f3a10d
commit d262041f33
21 changed files with 191 additions and 175 deletions

View File

@ -9,12 +9,12 @@ export default function dashboardItem({
}) {
return (
<div className="flex items-center">
<div className="w-[4.7rem] aspect-square flex justify-center items-center bg-primary/20 rounded-xl select-none">
<i className={`${icon} text-primary text-4xl drop-shadow`}></i>
<div className="w-[4rem] aspect-square flex justify-center items-center bg-primary/20 rounded-xl select-none">
<i className={`${icon} text-primary text-3xl drop-shadow`}></i>
</div>
<div className="ml-4 flex flex-col justify-center">
<p className="text-neutral text-xs tracking-wider">{name}</p>
<p className="font-thin text-6xl text-primary mt-0.5">{value}</p>
<p className="font-thin text-5xl text-primary mt-0.5">{value}</p>
</div>
</div>
);

View File

@ -13,7 +13,7 @@ export default function CardView({
isLoading?: boolean;
}) {
return (
<div className="grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
<div className="grid min-[1901px]:grid-cols-5 min-[1501px]:grid-cols-4 min-[881px]:grid-cols-3 min-[551px]:grid-cols-2 grid-cols-1 gap-5 pb-5">
{links.map((e, i) => {
return (
<LinkCard

View File

@ -128,11 +128,12 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
}
>
<div
className="rounded-2xl cursor-pointer"
className="rounded-2xl cursor-pointer h-full flex flex-col justify-between"
onClick={() =>
!editMode && window.open(generateLinkHref(link, account), "_blank")
}
>
<div>
<div className="relative rounded-t-2xl h-40 overflow-hidden">
{previewAvailable(link) ? (
<Image
@ -141,7 +142,9 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
height={720}
alt=""
className="rounded-t-2xl select-none object-cover z-10 h-40 w-full shadow opacity-80 scale-105"
style={{ filter: "blur(2px)" }}
style={
link.type !== "image" ? { filter: "blur(1px)" } : undefined
}
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
@ -153,25 +156,30 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
) : (
<div className="duration-100 h-40 bg-opacity-80 skeleton rounded-none"></div>
)}
{link.type !== "image" && (
<div className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center shadow rounded-md">
<LinkIcon link={link} />
</div>
)}
</div>
<hr className="divider my-0 last:hidden border-t border-neutral-content h-[1px]" />
</div>
<div className="p-3 mt-1">
<p className="truncate w-full pr-8 text-primary">
{unescapeString(link.name || link.description) || link.url}
<div className="flex flex-col justify-between h-full">
<div className="p-3 flex flex-col gap-2">
<p className="truncate w-full pr-8 text-primary text-sm">
{unescapeString(link.name)}
</p>
<LinkTypeBadge link={link} />
</div>
<div>
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
<div className="flex justify-between text-xs text-neutral px-3 pb-1">
<div className="cursor-pointer w-fit">
<div className="flex justify-between text-xs text-neutral px-3 pb-1 gap-2">
<div className="cursor-pointer truncate">
{collection && (
<LinkCollection link={link} collection={collection} />
)}
@ -179,6 +187,8 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
<LinkDate link={link} />
</div>
</div>
</div>
</div>
{showInfo && (
<div className="p-3 absolute z-30 top-0 left-0 right-0 bottom-0 bg-base-200 rounded-2xl fade-in overflow-y-auto">

View File

@ -25,14 +25,12 @@ export default function LinkTypeBadge({
onClick={(e) => {
e.stopPropagation();
}}
className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100"
className="flex gap-1 item-center select-none text-neutral hover:opacity-70 duration-100"
>
<i className="bi-link-45deg text-lg leading-none"></i>
<p className="text-xs truncate">{shortendURL}</p>
</Link>
) : (
<div className="badge badge-primary badge-sm my-1 select-none">
{link.type}
</div>
<div className="badge badge-primary badge-sm select-none">{link.type}</div>
);
}

View File

@ -30,7 +30,6 @@ type Props = {
};
export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
const viewMode = localStorage.getItem("viewMode") || "card";
const { collections } = useCollectionStore();
const { account } = useAccountStore();
@ -141,6 +140,9 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
height={720}
alt=""
className="rounded-t-2xl select-none object-cover z-10 h-40 w-full shadow opacity-80 scale-105"
style={
link.type !== "image" ? { filter: "blur(1px)" } : undefined
}
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
@ -150,28 +152,29 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
) : link.preview === "unavailable" ? null : (
<div className="duration-100 h-40 bg-opacity-80 skeleton rounded-none"></div>
)}
{link.type !== "image" && (
<div className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center shadow rounded-md">
<LinkIcon link={link} />
</div>
)}
</div>
{link.preview !== "unavailable" && (
<hr className="divider my-0 last:hidden border-t border-neutral-content h-[1px]" />
)}
<div className="p-3 mt-1 flex flex-col gap-2">
<div className="w-full pr-8">
<div
className={`rounded-t-2xl flex items-center justify-center shadow rounded-md float-left mr-2 ${
link.type === "url" ? "" : "hidden"
}`}
>
<LinkIcon link={link} size="small" className="mt-1" />
</div>
<p className="text-primary text-sm">{unescapeString(link.name)}</p>
<div className="p-3 flex flex-col gap-2">
<p className="hyphens-auto w-full pr-8 text-primary text-sm">
{unescapeString(link.name)}
</p>
<LinkTypeBadge link={link} />
{link.description && (
<p className="text-sm">{unescapeString(link.description)}</p>
<p className="hyphens-auto text-sm">
{unescapeString(link.description)}
</p>
)}
<LinkTypeBadge link={link} />
</div>
{link.tags[0] && (
<div className="flex gap-1 items-center flex-wrap">
@ -193,12 +196,8 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
<div className="flex justify-between gap-1 text-xs text-neutral px-3 pb-1">
<div className="cursor-pointer w-fit truncate">
{collection && (
<LinkCollection link={link} collection={collection} />
)}
</div>
<div className="flex flex-wrap justify-between text-xs text-neutral px-3 pb-1 w-full gap-x-2">
{collection && <LinkCollection link={link} collection={collection} />}
<LinkDate link={link} />
</div>
</div>

View File

@ -4,6 +4,7 @@ import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import toast from "react-hot-toast";
import Modal from "../Modal";
import { useRouter } from "next/router";
import Button from "../ui/Button";
type Props = {
onClose: Function;
@ -59,13 +60,10 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
&apos;Delete&apos; to bypass this confirmation in the future.
</p>
<button
className={`ml-auto btn w-fit text-white flex items-center gap-2 duration-100 bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer`}
onClick={deleteLink}
>
<Button className="ml-auto" intent="destructive" onClick={deleteLink}>
<i className="bi-trash text-xl" />
Delete
</button>
</Button>
</div>
</Modal>
);

View File

@ -1,6 +1,7 @@
import toast from "react-hot-toast";
import Modal from "../Modal";
import useUserStore from "@/store/admin/users";
import Button from "../ui/Button";
type Props = {
onClose: Function;
@ -38,13 +39,10 @@ export default function DeleteUserModal({ onClose, userId }: Props) {
</span>
</div>
<button
className={`ml-auto btn w-fit text-white flex items-center gap-2 duration-100 bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer`}
onClick={deleteUser}
>
<Button className="ml-auto" intent="destructive" onClick={deleteUser}>
<i className="bi-trash text-xl" />
Delete, I know what I&apos;m doing
</button>
</Button>
</div>
</Modal>
);

View File

@ -30,12 +30,11 @@ export default function EmailChangeVerificationModal({
"Updating this field will change your billing email on Stripe as well."}
</p>
{process.env.NEXT_PUBLIC_GOOGLE_ENABLED === "true" && (
<p>
If you change your email address, any existing Google SSO
If you change your email address, any existing{" "}
{process.env.NEXT_PUBLIC_GOOGLE_ENABLED === "true" && "Google"} SSO
connections will be removed.
</p>
)}
<div>
<p>Old Email</p>

View File

@ -5,6 +5,7 @@ import toast from "react-hot-toast";
import Modal from "../Modal";
import useTokenStore from "@/store/tokens";
import { dropdownTriggerer } from "@/lib/client/utils";
import Button from "../ui/Button";
type Props = {
onClose: Function;
@ -90,18 +91,19 @@ export default function NewTokenModal({ onClose }: Props) {
<p className="mb-2">Expires in</p>
<div className="dropdown dropdown-bottom dropdown-end w-full">
<div
<Button
tabIndex={0}
role="button"
intent="secondary"
onMouseDown={dropdownTriggerer}
className="btn btn-outline w-full sm:w-36 flex items-center btn-sm h-10"
className="whitespace-nowrap w-32"
>
{token.expires === TokenExpiry.sevenDays && "7 Days"}
{token.expires === TokenExpiry.oneMonth && "30 Days"}
{token.expires === TokenExpiry.twoMonths && "60 Days"}
{token.expires === TokenExpiry.threeMonths && "90 Days"}
{token.expires === TokenExpiry.never && "No Expiration"}
</div>
</Button>
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-xl w-full sm:w-52 mt-1">
<li>
<label

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 "./ui/ToggleDarkMode";
import ToggleDarkMode from "./ToggleDarkMode";
import NewLinkModal from "./ModalContent/NewLinkModal";
import NewCollectionModal from "./ModalContent/NewCollectionModal";
import UploadFileModal from "./ModalContent/UploadFileModal";

View File

@ -12,14 +12,15 @@ const buttonVariants = cva(
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",
destructive:
"bg-error text-white hover:bg-error/80 border border-neutral/60",
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",
small: "h-7 px-2",
medium: "h-10 px-4 py-2",
full: "px-4 py-2 w-full",
icon: "h-10 w-10",

View File

@ -40,6 +40,8 @@ export default function AuthRedirect({ children }: Props) {
{ path: "/collections", isProtected: true },
{ path: "/links", isProtected: true },
{ path: "/tags", isProtected: true },
{ path: "/preserved", isProtected: true },
{ path: "/admin", isProtected: true },
];
if (isPublicPage) {

View File

@ -1,5 +1,5 @@
import Navbar from "@/components/Navbar";
import Announcement from "@/components/Announcement";
import Announcement from "@/components/AnnouncementBar";
import Sidebar from "@/components/Sidebar";
import { ReactNode, useEffect, useState } from "react";
import getLatestVersion from "@/lib/client/getLatestVersion";

View File

@ -94,10 +94,11 @@ export default async function deleteUserById(
// Delete subscription
if (process.env.STRIPE_SECRET_KEY)
await prisma.subscription.delete({
await prisma.subscription
.delete({
where: { userId },
});
// .catch((err) => console.log(err));
})
.catch((err) => console.log(err));
await prisma.usersAndCollections.deleteMany({
where: {

View File

@ -120,8 +120,7 @@ export default function Dashboard() {
<ViewDropdown viewMode={viewMode} setViewMode={setViewMode} />
</div>
<div>
<div className="flex justify-evenly flex-col xl:flex-row xl:items-center gap-2 xl:w-full h-full rounded-2xl p-8 border border-neutral-content bg-base-200">
<div className="flex justify-evenly flex-col xl:flex-row xl:items-center gap-2 xl:w-full h-full rounded-2xl p-5 border border-neutral-content bg-base-200">
<DashboardItem
name={numberOfLinks === 1 ? "Link" : "Links"}
value={numberOfLinks}
@ -144,7 +143,6 @@ export default function Dashboard() {
icon={"bi-hash"}
/>
</div>
</div>
<div className="flex justify-between items-center">
<div className="flex gap-2 items-center">

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/ui/ToggleDarkMode";
import ToggleDarkMode from "@/components/ToggleDarkMode";
import getPublicUserData from "@/lib/client/getPublicUserData";
import Image from "next/image";
import Link from "next/link";

View File

@ -13,6 +13,7 @@ import Link from "next/link";
import Checkbox from "@/components/Checkbox";
import { dropdownTriggerer } from "@/lib/client/utils";
import EmailChangeVerificationModal from "@/components/ModalContent/EmailChangeVerificationModal";
import Button from "@/components/ui/Button";
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
@ -203,28 +204,29 @@ export default function Account() {
<div className="sm:row-span-2 sm:justify-self-center my-3">
<p className="mb-2 sm:text-center">Profile Photo</p>
<div className="w-28 h-28 flex items-center justify-center rounded-full relative">
<div className="w-28 h-28 flex gap-3 sm:flex-col items-center">
<ProfilePhoto
priority={true}
src={user.image ? user.image : undefined}
large={true}
/>
{user.image && (
<div
onClick={() =>
setUser({
...user,
image: "",
})
}
className="absolute top-1 left-1 btn btn-xs btn-circle btn-neutral btn-outline bg-base-100"
<div className="dropdown dropdown-bottom">
<Button
tabIndex={0}
role="button"
size="small"
intent="secondary"
onMouseDown={dropdownTriggerer}
className="text-sm"
>
<i className="bi-x"></i>
</div>
)}
<div className="absolute -bottom-3 left-0 right-0 mx-auto w-fit text-center">
<label className="btn btn-xs btn-neutral btn-outline bg-base-100">
Browse...
<i className="bi-pencil-square text-md duration-100"></i>
Edit
</Button>
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
<li>
<label tabIndex={0} role="button">
Upload a new photo...
<input
type="file"
name="photo"
@ -234,6 +236,24 @@ export default function Account() {
onChange={handleImageUpload}
/>
</label>
</li>
{user.image && (
<li>
<div
tabIndex={0}
role="button"
onClick={() =>
setUser({
...user,
image: "",
})
}
>
Remove Photo
</div>
</li>
)}
</ul>
</div>
</div>
</div>
@ -293,16 +313,18 @@ export default function Account() {
<div>
<p className="mb-2">Import your data from other platforms.</p>
<div className="dropdown dropdown-bottom">
<div
<Button
tabIndex={0}
role="button"
intent="secondary"
onMouseDown={dropdownTriggerer}
className="flex gap-2 text-sm btn btn-outline btn-neutral group"
className="text-sm"
id="import-dropdown"
>
<i className="bi-cloud-upload text-xl duration-100"></i>
<p>Import From</p>
</div>
Import From
</Button>
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
<li>
<label
@ -351,7 +373,7 @@ export default function Account() {
<div>
<p className="mb-2">Download your data instantly.</p>
<Link className="w-fit" href="/api/v1/migration">
<div className="flex w-fit gap-2 text-sm btn btn-outline btn-neutral group">
<div className="select-none relative duration-200 rounded-lg text-sm text-center w-fit flex justify-center items-center gap-2 disabled:pointer-events-none disabled:opacity-50 bg-neutral-content text-secondary-foreground hover:bg-neutral-content/80 border border-neutral/30 h-10 px-4 py-2">
<i className="bi-cloud-download text-xl duration-100"></i>
<p>Export Data</p>
</div>
@ -374,17 +396,12 @@ export default function Account() {
archived data you own.{" "}
{process.env.NEXT_PUBLIC_STRIPE
? "It will also cancel your subscription. "
: undefined}{" "}
You will be prompted to enter your password before the deletion
process.
: undefined}
</p>
</div>
<Link
href="/settings/delete"
className="text-white w-full sm:w-fit flex items-center gap-2 py-2 px-4 rounded-md text-lg tracking-wide select-none font-semibold duration-100 bg-red-500 hover:bg-red-400 cursor-pointer"
>
<p className="text-center w-full">Delete Your Account</p>
<Link href="/settings/delete" className="underline">
Account deletion page
</Link>
</div>

View File

@ -21,6 +21,7 @@ export default function Billing() {
<a
href={process.env.NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL}
className="underline"
target="_blank"
>
Billing Portal
</a>
@ -30,10 +31,7 @@ export default function Billing() {
<p className="text-md">
If you still need help or encountered any issues, feel free to reach
out to us at:{" "}
<a
className="font-semibold underline"
href="mailto:support@linkwarden.app"
>
<a className="font-semibold" href="mailto:support@linkwarden.app">
support@linkwarden.app
</a>
</p>

View File

@ -4,6 +4,7 @@ import TextInput from "@/components/TextInput";
import CenteredForm from "@/layouts/CenteredForm";
import { signOut, useSession } from "next-auth/react";
import Link from "next/link";
import Button from "@/components/ui/Button";
const keycloakEnabled = process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true";
const authentikEnabled = process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED === "true";
@ -135,20 +136,14 @@ export default function Delete() {
</fieldset>
) : undefined}
<button
className={`mx-auto text-white flex items-center gap-2 py-1 px-3 rounded-md text-lg tracking-wide select-none font-semibold duration-100 w-fit ${
submitLoader
? "bg-red-400 cursor-auto"
: "bg-red-500 hover:bg-red-400 cursor-pointer"
}`}
onClick={() => {
if (!submitLoader) {
submit();
}
}}
<Button
className="mx-auto"
intent="destructive"
loading={submitLoader}
onClick={submit}
>
<p className="text-center w-full">Delete Your Account</p>
</button>
</Button>
</div>
</CenteredForm>
);