refactor code to improve readability and maintainability + redesigned announcement bar
This commit is contained in:
parent
a498f3a10d
commit
d262041f33
|
@ -9,12 +9,12 @@ export default function dashboardItem({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="w-[4.7rem] aspect-square flex justify-center items-center bg-primary/20 rounded-xl select-none">
|
<div className="w-[4rem] aspect-square flex justify-center items-center bg-primary/20 rounded-xl select-none">
|
||||||
<i className={`${icon} text-primary text-4xl drop-shadow`}></i>
|
<i className={`${icon} text-primary text-3xl drop-shadow`}></i>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 flex flex-col justify-center">
|
<div className="ml-4 flex flex-col justify-center">
|
||||||
<p className="text-neutral text-xs tracking-wider">{name}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default function CardView({
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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) => {
|
{links.map((e, i) => {
|
||||||
return (
|
return (
|
||||||
<LinkCard
|
<LinkCard
|
||||||
|
|
|
@ -128,11 +128,12 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="rounded-2xl cursor-pointer"
|
className="rounded-2xl cursor-pointer h-full flex flex-col justify-between"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
!editMode && window.open(generateLinkHref(link, account), "_blank")
|
!editMode && window.open(generateLinkHref(link, account), "_blank")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
<div className="relative rounded-t-2xl h-40 overflow-hidden">
|
<div className="relative rounded-t-2xl h-40 overflow-hidden">
|
||||||
{previewAvailable(link) ? (
|
{previewAvailable(link) ? (
|
||||||
<Image
|
<Image
|
||||||
|
@ -141,7 +142,9 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
||||||
height={720}
|
height={720}
|
||||||
alt=""
|
alt=""
|
||||||
className="rounded-t-2xl select-none object-cover z-10 h-40 w-full shadow opacity-80 scale-105"
|
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"
|
draggable="false"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const target = e.target as HTMLElement;
|
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>
|
<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">
|
<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} />
|
<LinkIcon link={link} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="divider my-0 last:hidden border-t border-neutral-content h-[1px]" />
|
<hr className="divider my-0 last:hidden border-t border-neutral-content h-[1px]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="p-3 mt-1">
|
<div className="flex flex-col justify-between h-full">
|
||||||
<p className="truncate w-full pr-8 text-primary">
|
<div className="p-3 flex flex-col gap-2">
|
||||||
{unescapeString(link.name || link.description) || link.url}
|
<p className="truncate w-full pr-8 text-primary text-sm">
|
||||||
|
{unescapeString(link.name)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<LinkTypeBadge link={link} />
|
<LinkTypeBadge link={link} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
|
<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="flex justify-between text-xs text-neutral px-3 pb-1 gap-2">
|
||||||
<div className="cursor-pointer w-fit">
|
<div className="cursor-pointer truncate">
|
||||||
{collection && (
|
{collection && (
|
||||||
<LinkCollection link={link} collection={collection} />
|
<LinkCollection link={link} collection={collection} />
|
||||||
)}
|
)}
|
||||||
|
@ -179,6 +187,8 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
||||||
<LinkDate link={link} />
|
<LinkDate link={link} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{showInfo && (
|
{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">
|
<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">
|
||||||
|
|
|
@ -25,14 +25,12 @@ export default function LinkTypeBadge({
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
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>
|
<i className="bi-link-45deg text-lg leading-none"></i>
|
||||||
<p className="text-xs truncate">{shortendURL}</p>
|
<p className="text-xs truncate">{shortendURL}</p>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div className="badge badge-primary badge-sm my-1 select-none">
|
<div className="badge badge-primary badge-sm select-none">{link.type}</div>
|
||||||
{link.type}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
|
export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
|
||||||
const viewMode = localStorage.getItem("viewMode") || "card";
|
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
const { account } = useAccountStore();
|
const { account } = useAccountStore();
|
||||||
|
|
||||||
|
@ -141,6 +140,9 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
|
||||||
height={720}
|
height={720}
|
||||||
alt=""
|
alt=""
|
||||||
className="rounded-t-2xl select-none object-cover z-10 h-40 w-full shadow opacity-80 scale-105"
|
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"
|
draggable="false"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
|
@ -150,28 +152,29 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
|
||||||
) : link.preview === "unavailable" ? null : (
|
) : link.preview === "unavailable" ? null : (
|
||||||
<div className="duration-100 h-40 bg-opacity-80 skeleton rounded-none"></div>
|
<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>
|
</div>
|
||||||
|
|
||||||
{link.preview !== "unavailable" && (
|
{link.preview !== "unavailable" && (
|
||||||
<hr className="divider my-0 last:hidden border-t border-neutral-content h-[1px]" />
|
<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="p-3 flex flex-col gap-2">
|
||||||
<div className="w-full pr-8">
|
<p className="hyphens-auto w-full pr-8 text-primary text-sm">
|
||||||
<div
|
{unescapeString(link.name)}
|
||||||
className={`rounded-t-2xl flex items-center justify-center shadow rounded-md float-left mr-2 ${
|
</p>
|
||||||
link.type === "url" ? "" : "hidden"
|
|
||||||
}`}
|
<LinkTypeBadge link={link} />
|
||||||
>
|
|
||||||
<LinkIcon link={link} size="small" className="mt-1" />
|
|
||||||
</div>
|
|
||||||
<p className="text-primary text-sm">{unescapeString(link.name)}</p>
|
|
||||||
|
|
||||||
{link.description && (
|
{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] && (
|
{link.tags[0] && (
|
||||||
<div className="flex gap-1 items-center flex-wrap">
|
<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]" />
|
<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="flex flex-wrap justify-between text-xs text-neutral px-3 pb-1 w-full gap-x-2">
|
||||||
<div className="cursor-pointer w-fit truncate">
|
{collection && <LinkCollection link={link} collection={collection} />}
|
||||||
{collection && (
|
|
||||||
<LinkCollection link={link} collection={collection} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<LinkDate link={link} />
|
<LinkDate link={link} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import Button from "../ui/Button";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: Function;
|
onClose: Function;
|
||||||
|
@ -59,13 +60,10 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
|
||||||
'Delete' to bypass this confirmation in the future.
|
'Delete' to bypass this confirmation in the future.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<Button className="ml-auto" intent="destructive" onClick={deleteLink}>
|
||||||
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}
|
|
||||||
>
|
|
||||||
<i className="bi-trash text-xl" />
|
<i className="bi-trash text-xl" />
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
import useUserStore from "@/store/admin/users";
|
import useUserStore from "@/store/admin/users";
|
||||||
|
import Button from "../ui/Button";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: Function;
|
onClose: Function;
|
||||||
|
@ -38,13 +39,10 @@ export default function DeleteUserModal({ onClose, userId }: Props) {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button className="ml-auto" intent="destructive" onClick={deleteUser}>
|
||||||
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}
|
|
||||||
>
|
|
||||||
<i className="bi-trash text-xl" />
|
<i className="bi-trash text-xl" />
|
||||||
Delete, I know what I'm doing
|
Delete, I know what I'm doing
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -30,12 +30,11 @@ export default function EmailChangeVerificationModal({
|
||||||
"Updating this field will change your billing email on Stripe as well."}
|
"Updating this field will change your billing email on Stripe as well."}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{process.env.NEXT_PUBLIC_GOOGLE_ENABLED === "true" && (
|
|
||||||
<p>
|
<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.
|
connections will be removed.
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>Old Email</p>
|
<p>Old Email</p>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import toast from "react-hot-toast";
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
import useTokenStore from "@/store/tokens";
|
import useTokenStore from "@/store/tokens";
|
||||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||||
|
import Button from "../ui/Button";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: Function;
|
onClose: Function;
|
||||||
|
@ -90,18 +91,19 @@ export default function NewTokenModal({ onClose }: Props) {
|
||||||
<p className="mb-2">Expires in</p>
|
<p className="mb-2">Expires in</p>
|
||||||
|
|
||||||
<div className="dropdown dropdown-bottom dropdown-end w-full">
|
<div className="dropdown dropdown-bottom dropdown-end w-full">
|
||||||
<div
|
<Button
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
|
intent="secondary"
|
||||||
onMouseDown={dropdownTriggerer}
|
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.sevenDays && "7 Days"}
|
||||||
{token.expires === TokenExpiry.oneMonth && "30 Days"}
|
{token.expires === TokenExpiry.oneMonth && "30 Days"}
|
||||||
{token.expires === TokenExpiry.twoMonths && "60 Days"}
|
{token.expires === TokenExpiry.twoMonths && "60 Days"}
|
||||||
{token.expires === TokenExpiry.threeMonths && "90 Days"}
|
{token.expires === TokenExpiry.threeMonths && "90 Days"}
|
||||||
{token.expires === TokenExpiry.never && "No Expiration"}
|
{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">
|
<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>
|
<li>
|
||||||
<label
|
<label
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Sidebar from "@/components/Sidebar";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import SearchBar from "@/components/SearchBar";
|
import SearchBar from "@/components/SearchBar";
|
||||||
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
||||||
import ToggleDarkMode from "./ui/ToggleDarkMode";
|
import ToggleDarkMode from "./ToggleDarkMode";
|
||||||
import NewLinkModal from "./ModalContent/NewLinkModal";
|
import NewLinkModal from "./ModalContent/NewLinkModal";
|
||||||
import NewCollectionModal from "./ModalContent/NewCollectionModal";
|
import NewCollectionModal from "./ModalContent/NewCollectionModal";
|
||||||
import UploadFileModal from "./ModalContent/UploadFileModal";
|
import UploadFileModal from "./ModalContent/UploadFileModal";
|
||||||
|
|
|
@ -12,14 +12,15 @@ const buttonVariants = cva(
|
||||||
primary: "bg-primary text-primary-content hover:bg-primary/80",
|
primary: "bg-primary text-primary-content hover:bg-primary/80",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-neutral-content text-secondary-foreground hover:bg-neutral-content/80 border border-neutral/30",
|
"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:
|
outline:
|
||||||
"border border-input bg-background hover:bg-accent hover:text-accent-content",
|
"border border-input bg-background hover:bg-accent hover:text-accent-content",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
small: "h-9 px-3",
|
small: "h-7 px-2",
|
||||||
medium: "h-10 px-4 py-2",
|
medium: "h-10 px-4 py-2",
|
||||||
full: "px-4 py-2 w-full",
|
full: "px-4 py-2 w-full",
|
||||||
icon: "h-10 w-10",
|
icon: "h-10 w-10",
|
||||||
|
|
|
@ -40,6 +40,8 @@ export default function AuthRedirect({ children }: Props) {
|
||||||
{ path: "/collections", isProtected: true },
|
{ path: "/collections", isProtected: true },
|
||||||
{ path: "/links", isProtected: true },
|
{ path: "/links", isProtected: true },
|
||||||
{ path: "/tags", isProtected: true },
|
{ path: "/tags", isProtected: true },
|
||||||
|
{ path: "/preserved", isProtected: true },
|
||||||
|
{ path: "/admin", isProtected: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isPublicPage) {
|
if (isPublicPage) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Navbar from "@/components/Navbar";
|
import Navbar from "@/components/Navbar";
|
||||||
import Announcement from "@/components/Announcement";
|
import Announcement from "@/components/AnnouncementBar";
|
||||||
import Sidebar from "@/components/Sidebar";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import getLatestVersion from "@/lib/client/getLatestVersion";
|
import getLatestVersion from "@/lib/client/getLatestVersion";
|
||||||
|
|
|
@ -94,10 +94,11 @@ export default async function deleteUserById(
|
||||||
|
|
||||||
// Delete subscription
|
// Delete subscription
|
||||||
if (process.env.STRIPE_SECRET_KEY)
|
if (process.env.STRIPE_SECRET_KEY)
|
||||||
await prisma.subscription.delete({
|
await prisma.subscription
|
||||||
|
.delete({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
});
|
})
|
||||||
// .catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
|
|
||||||
await prisma.usersAndCollections.deleteMany({
|
await prisma.usersAndCollections.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -120,8 +120,7 @@ export default function Dashboard() {
|
||||||
<ViewDropdown viewMode={viewMode} setViewMode={setViewMode} />
|
<ViewDropdown viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
</div>
|
</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-5 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-8 border border-neutral-content bg-base-200">
|
|
||||||
<DashboardItem
|
<DashboardItem
|
||||||
name={numberOfLinks === 1 ? "Link" : "Links"}
|
name={numberOfLinks === 1 ? "Link" : "Links"}
|
||||||
value={numberOfLinks}
|
value={numberOfLinks}
|
||||||
|
@ -144,7 +143,6 @@ export default function Dashboard() {
|
||||||
icon={"bi-hash"}
|
icon={"bi-hash"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Head from "next/head";
|
||||||
import useLinks from "@/hooks/useLinks";
|
import useLinks from "@/hooks/useLinks";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||||
import ToggleDarkMode from "@/components/ui/ToggleDarkMode";
|
import ToggleDarkMode from "@/components/ToggleDarkMode";
|
||||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
|
@ -13,6 +13,7 @@ import Link from "next/link";
|
||||||
import Checkbox from "@/components/Checkbox";
|
import Checkbox from "@/components/Checkbox";
|
||||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||||
import EmailChangeVerificationModal from "@/components/ModalContent/EmailChangeVerificationModal";
|
import EmailChangeVerificationModal from "@/components/ModalContent/EmailChangeVerificationModal";
|
||||||
|
import Button from "@/components/ui/Button";
|
||||||
|
|
||||||
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
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">
|
<div className="sm:row-span-2 sm:justify-self-center my-3">
|
||||||
<p className="mb-2 sm:text-center">Profile Photo</p>
|
<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
|
<ProfilePhoto
|
||||||
priority={true}
|
priority={true}
|
||||||
src={user.image ? user.image : undefined}
|
src={user.image ? user.image : undefined}
|
||||||
large={true}
|
large={true}
|
||||||
/>
|
/>
|
||||||
{user.image && (
|
|
||||||
<div
|
<div className="dropdown dropdown-bottom">
|
||||||
onClick={() =>
|
<Button
|
||||||
setUser({
|
tabIndex={0}
|
||||||
...user,
|
role="button"
|
||||||
image: "",
|
size="small"
|
||||||
})
|
intent="secondary"
|
||||||
}
|
onMouseDown={dropdownTriggerer}
|
||||||
className="absolute top-1 left-1 btn btn-xs btn-circle btn-neutral btn-outline bg-base-100"
|
className="text-sm"
|
||||||
>
|
>
|
||||||
<i className="bi-x"></i>
|
<i className="bi-pencil-square text-md duration-100"></i>
|
||||||
</div>
|
Edit
|
||||||
)}
|
</Button>
|
||||||
<div className="absolute -bottom-3 left-0 right-0 mx-auto w-fit text-center">
|
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
|
||||||
<label className="btn btn-xs btn-neutral btn-outline bg-base-100">
|
<li>
|
||||||
Browse...
|
<label tabIndex={0} role="button">
|
||||||
|
Upload a new photo...
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
name="photo"
|
name="photo"
|
||||||
|
@ -234,6 +236,24 @@ export default function Account() {
|
||||||
onChange={handleImageUpload}
|
onChange={handleImageUpload}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
</li>
|
||||||
|
{user.image && (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
onClick={() =>
|
||||||
|
setUser({
|
||||||
|
...user,
|
||||||
|
image: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Remove Photo
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -293,16 +313,18 @@ export default function Account() {
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2">Import your data from other platforms.</p>
|
<p className="mb-2">Import your data from other platforms.</p>
|
||||||
<div className="dropdown dropdown-bottom">
|
<div className="dropdown dropdown-bottom">
|
||||||
<div
|
<Button
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
|
intent="secondary"
|
||||||
onMouseDown={dropdownTriggerer}
|
onMouseDown={dropdownTriggerer}
|
||||||
className="flex gap-2 text-sm btn btn-outline btn-neutral group"
|
className="text-sm"
|
||||||
id="import-dropdown"
|
id="import-dropdown"
|
||||||
>
|
>
|
||||||
<i className="bi-cloud-upload text-xl duration-100"></i>
|
<i className="bi-cloud-upload text-xl duration-100"></i>
|
||||||
<p>Import From</p>
|
Import From
|
||||||
</div>
|
</Button>
|
||||||
|
|
||||||
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
|
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
|
||||||
<li>
|
<li>
|
||||||
<label
|
<label
|
||||||
|
@ -351,7 +373,7 @@ export default function Account() {
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2">Download your data instantly.</p>
|
<p className="mb-2">Download your data instantly.</p>
|
||||||
<Link className="w-fit" href="/api/v1/migration">
|
<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>
|
<i className="bi-cloud-download text-xl duration-100"></i>
|
||||||
<p>Export Data</p>
|
<p>Export Data</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -374,17 +396,12 @@ export default function Account() {
|
||||||
archived data you own.{" "}
|
archived data you own.{" "}
|
||||||
{process.env.NEXT_PUBLIC_STRIPE
|
{process.env.NEXT_PUBLIC_STRIPE
|
||||||
? "It will also cancel your subscription. "
|
? "It will also cancel your subscription. "
|
||||||
: undefined}{" "}
|
: undefined}
|
||||||
You will be prompted to enter your password before the deletion
|
|
||||||
process.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link href="/settings/delete" className="underline">
|
||||||
href="/settings/delete"
|
Account deletion page
|
||||||
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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default function Billing() {
|
||||||
<a
|
<a
|
||||||
href={process.env.NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL}
|
href={process.env.NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL}
|
||||||
className="underline"
|
className="underline"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
Billing Portal
|
Billing Portal
|
||||||
</a>
|
</a>
|
||||||
|
@ -30,10 +31,7 @@ export default function Billing() {
|
||||||
<p className="text-md">
|
<p className="text-md">
|
||||||
If you still need help or encountered any issues, feel free to reach
|
If you still need help or encountered any issues, feel free to reach
|
||||||
out to us at:{" "}
|
out to us at:{" "}
|
||||||
<a
|
<a className="font-semibold" href="mailto:support@linkwarden.app">
|
||||||
className="font-semibold underline"
|
|
||||||
href="mailto:support@linkwarden.app"
|
|
||||||
>
|
|
||||||
support@linkwarden.app
|
support@linkwarden.app
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import TextInput from "@/components/TextInput";
|
||||||
import CenteredForm from "@/layouts/CenteredForm";
|
import CenteredForm from "@/layouts/CenteredForm";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import Button from "@/components/ui/Button";
|
||||||
|
|
||||||
const keycloakEnabled = process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true";
|
const keycloakEnabled = process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED === "true";
|
||||||
const authentikEnabled = process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED === "true";
|
const authentikEnabled = process.env.NEXT_PUBLIC_AUTHENTIK_ENABLED === "true";
|
||||||
|
@ -135,20 +136,14 @@ export default function Delete() {
|
||||||
</fieldset>
|
</fieldset>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
<button
|
<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 ${
|
className="mx-auto"
|
||||||
submitLoader
|
intent="destructive"
|
||||||
? "bg-red-400 cursor-auto"
|
loading={submitLoader}
|
||||||
: "bg-red-500 hover:bg-red-400 cursor-pointer"
|
onClick={submit}
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (!submitLoader) {
|
|
||||||
submit();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<p className="text-center w-full">Delete Your Account</p>
|
<p className="text-center w-full">Delete Your Account</p>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CenteredForm>
|
</CenteredForm>
|
||||||
);
|
);
|
||||||
|
|
Ŝarĝante…
Reference in New Issue