many visual changes and improvements

This commit is contained in:
Daniel 2023-06-05 13:24:43 +03:30
parent 6a4f21fc0a
commit a9d7303359
24 changed files with 311 additions and 194 deletions

View File

@ -85,7 +85,7 @@ export default function ({ link, count }: Props) {
<div className="flex gap-3 items-center flex-wrap my-3">
<Link href={`/collections/${link.collection.id}`}>
<div className="flex items-center gap-1 cursor-pointer hover:opacity-60 duration-100">
<FontAwesomeIcon icon={faFolder} className="w-4 text-sky-300" />
<FontAwesomeIcon icon={faFolder} className="w-4 text-sky-500" />
<p className="text-sky-900">{link.collection.name}</p>
</div>
</Link>

View File

@ -24,7 +24,7 @@ export default function Dropdown({ onClickOutside, className, items }: Props) {
return (
<ClickAwayHandler
onClickOutside={onClickOutside}
className={`${className} border border-sky-100 py-1 shadow-md bg-gray-50 rounded-md flex flex-col z-10`}
className={`${className} py-1 shadow-md bg-gray-50 rounded-md flex flex-col z-10`}
>
{items.map((e, i) => {
const inner = (

View File

@ -0,0 +1,57 @@
import React, { SetStateAction } from "react";
import ClickAwayHandler from "./ClickAwayHandler";
import Checkbox from "./Checkbox";
import { SearchSettings } from "@/types/global";
type Props = {
setFilterDropdown: (value: SetStateAction<boolean>) => void;
toggleCheckbox: (
name: "name" | "title" | "url" | "collection" | "tags"
) => void;
searchSettings: SearchSettings;
};
export default function FilterSearchDropdown({
setFilterDropdown,
toggleCheckbox,
searchSettings,
}: Props) {
return (
<ClickAwayHandler
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "filter-dropdown") setFilterDropdown(false);
}}
className="absolute top-8 right-0 shadow-md bg-gray-50 rounded-md p-2 z-20 w-40"
>
<p className="mb-2 text-sky-900 text-center font-semibold">Filter by</p>
<div className="flex flex-col gap-2">
<Checkbox
label="Name"
state={searchSettings.filter.name}
onClick={() => toggleCheckbox("name")}
/>
<Checkbox
label="Link"
state={searchSettings.filter.url}
onClick={() => toggleCheckbox("url")}
/>
<Checkbox
label="Title"
state={searchSettings.filter.title}
onClick={() => toggleCheckbox("title")}
/>
<Checkbox
label="Collection"
state={searchSettings.filter.collection}
onClick={() => toggleCheckbox("collection")}
/>
<Checkbox
label="Tags"
state={searchSettings.filter.tags}
onClick={() => toggleCheckbox("tags")}
/>
</div>
</ClickAwayHandler>
);
}

View File

@ -78,22 +78,26 @@ export default function ({ link, count }: Props) {
<div className="flex justify-between gap-5 w-full h-full z-0">
<div className="flex flex-col justify-between">
<div className="flex items-baseline gap-1">
<p className="text-sm text-sky-300 font-bold">{count + 1}.</p>
<p className="text-sm text-sky-400 font-bold">{count + 1}.</p>
<p className="text-lg text-sky-600 font-bold">{link.name}</p>
</div>
<p className="text-sky-400 text-sm font-medium">{link.title}</p>
<div className="flex gap-3 items-center flex-wrap my-3">
<Link href={`/collections/${link.collection.id}`}>
<div className="flex items-center gap-1 cursor-pointer hover:opacity-60 duration-100">
<FontAwesomeIcon icon={faFolder} className="w-4 text-sky-300" />
<FontAwesomeIcon
icon={faFolder}
className="w-4 h-4 mt-1"
style={{ color: link.collection.color }}
/>
<p className="text-sky-900">{link.collection.name}</p>
</div>
</Link>
<div className="flex gap-1 items-center flex-wrap">
<div className="flex gap-1 items-center flex-wrap mt-1">
{link.tags.map((e, i) => (
<Link key={i} href={`/tags/${e.id}`}>
<p className="px-2 py-1 bg-sky-200 text-sky-700 text-xs rounded-3xl cursor-pointer hover:bg-sky-100 duration-100">
<p className="px-2 py-1 bg-sky-200 text-sky-700 text-xs rounded-3xl cursor-pointer hover:opacity-60 duration-100">
{e.name}
</p>
</Link>

View File

@ -34,7 +34,7 @@ export default function ChangePassword({
<div className="max-w-sm mx-auto flex flex-col gap-3">
<p className="text-xl text-sky-500 mb-2 text-center">Change Password</p>
<p className="text-sm font-bold text-sky-300">Old Password</p>
<p className="text-sm text-sky-500">Old Password</p>
<input
value={oldPassword}
@ -42,7 +42,7 @@ export default function ChangePassword({
type="text"
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
<p className="text-sm font-bold text-sky-300">New Password</p>
<p className="text-sm text-sky-500">New Password</p>
<input
value={newPassword1}
@ -50,7 +50,7 @@ export default function ChangePassword({
type="text"
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
<p className="text-sm font-bold text-sky-300">Re-enter New Password</p>
<p className="text-sm text-sky-500">Re-enter New Password</p>
<input
value={newPassword2}
@ -60,7 +60,7 @@ export default function ChangePassword({
/>
<div
className="mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none font-bold duration-100 bg-sky-500 hover:bg-sky-400 cursor-pointer"
className="mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none duration-100 bg-sky-500 hover:bg-sky-400 cursor-pointer"
onClick={submit}
>
Save

View File

@ -47,7 +47,7 @@ export default function CollectionInfo({
<div className="flex flex-col sm:flex-row gap-3">
<div className="w-full">
<p className="text-sm font-bold text-sky-300 mb-2">
<p className="text-sm text-sky-500 mb-2">
Name
<RequiredBadge />
</p>
@ -63,9 +63,7 @@ export default function CollectionInfo({
/>
<div className="color-picker flex justify-between">
<div className="flex flex-col justify-between items-center w-32">
<p className="text-sm w-full font-bold text-sky-300 mb-2">
Icon Color
</p>
<p className="text-sm w-full text-sky-500 mb-2">Icon Color</p>
<div style={{ color: collection.color }}>
<FontAwesomeIcon
icon={faFolder}
@ -73,9 +71,9 @@ export default function CollectionInfo({
/>
</div>
<div
className="py-1 px-2 rounded-md text-xs font-semibold cursor-pointer text-gray-500 hover:bg-slate-200 duration-100"
className="py-1 px-2 rounded-md text-xs font-semibold cursor-pointer text-sky-500 hover:bg-slate-200 duration-100"
onClick={() =>
setCollection({ ...collection, color: "#7dd3fc" })
setCollection({ ...collection, color: "#0ea5e9" })
}
>
Reset
@ -90,7 +88,7 @@ export default function CollectionInfo({
</div>
<div className="w-full">
<p className="text-sm font-bold text-sky-300 mb-2">Description</p>
<p className="text-sm text-sky-500 mb-2">Description</p>
<textarea
className="w-full h-[11.4rem] resize-none border rounded-md duration-100 bg-white p-3 outline-none border-sky-100 focus:border-sky-500"
placeholder="The purpose of this Collection..."

View File

@ -77,7 +77,7 @@ export default function TeamManagement({
Sharing & Collaboration
</p>
<p className="text-sm font-bold text-sky-300">Make Public</p>
<p className="text-sm text-sky-500">Make Public</p>
<Checkbox
label="Make this a public collection."
@ -93,7 +93,9 @@ export default function TeamManagement({
{collection.isPublic ? (
<div>
<p className="mb-2 text-gray-500">Public Link (Click to copy)</p>
<p className="text-sm text-sky-500 mb-2">
Public Link (Click to copy)
</p>
<div
onClick={() => {
try {
@ -113,7 +115,7 @@ export default function TeamManagement({
<hr />
<p className="text-sm font-bold text-sky-300">Member Management</p>
<p className="text-sm text-sky-500">Member Management</p>
<div className="flex items-center gap-2">
<input
@ -202,10 +204,10 @@ export default function TeamManagement({
</div>
<div className="flex sm:block items-center gap-5">
<div>
<p className="font-bold text-sm text-gray-500">
<p className="font-bold text-sm text-sky-500">
Permissions
</p>
<p className="text-xs text-gray-400 mb-2">
<p className="text-xs text-gray-500 mb-2">
(Click to toggle.)
</p>
</div>

View File

@ -115,7 +115,7 @@ export default function EditLink({
<div className="grid sm:grid-cols-2 gap-3">
<div>
<p className="text-sm font-bold text-sky-300 mb-2">
<p className="text-sm text-sky-500 mb-2">
Name
<RequiredBadge />
</p>
@ -130,7 +130,7 @@ export default function EditLink({
{method === "CREATE" ? (
<div>
<p className="text-sm font-bold text-sky-300 mb-2">
<p className="text-sm text-sky-500 mb-2">
URL
<RequiredBadge />
</p>
@ -145,7 +145,7 @@ export default function EditLink({
) : null}
<div>
<p className="text-sm font-bold text-sky-300 mb-2">
<p className="text-sm text-sky-500 mb-2">
Collection
<RequiredBadge />
</p>
@ -167,7 +167,7 @@ export default function EditLink({
</div>
<div className={method === "UPDATE" ? "sm:col-span-2" : ""}>
<p className="text-sm font-bold text-sky-300 mb-2">Tags</p>
<p className="text-sm text-sky-500 mb-2">Tags</p>
<TagSelection
onChange={setTags}
defaultValue={link.tags.map((e) => {

View File

@ -95,12 +95,10 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
<p className="text-xl text-sky-500 mb-2 text-center">Settings</p>
<p className="text-sky-600">Profile Settings</p>
<div className="grid sm:grid-cols-2 gap-3 auto-rows-auto">
<div className="flex flex-col gap-3">
<div>
<p className="text-sm font-bold text-sky-300 mb-2">Display Name</p>
<p className="text-sm text-sky-500 mb-2">Display Name</p>
<input
type="text"
value={user.name}
@ -110,7 +108,7 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
</div>
<div>
<p className="text-sm font-bold text-sky-300 mb-2">Email</p>
<p className="text-sm text-sky-500 mb-2">Email</p>
<input
type="text"
value={user.email}
@ -120,7 +118,7 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
</div>
<div>
<p className="text-sm font-bold text-sky-300 mb-2">Password</p>
<p className="text-sm text-sky-500 mb-2">Password</p>
<div className="w-fit" onClick={togglePasswordFormModal}>
<div className="border border-sky-100 rounded-md bg-white px-2 py-1 text-center select-none cursor-pointer text-sky-900 duration-100 hover:border-sky-500">
@ -139,7 +137,7 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
</div>
<div className="sm:row-span-2 sm:justify-self-center mb-3">
<p className="text-sm font-bold text-sky-300 mb-2 sm:text-center">
<p className="text-sm text-sky-500 mb-2 sm:text-center">
Profile Photo
</p>
<div className="w-28 h-28 flex items-center justify-center border border-sky-100 rounded-full relative">
@ -202,7 +200,7 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
<hr />
<p className="text-sky-600">Privacy Settings</p>
<p className="text-sm text-sky-500 mb-2">Profile Visibility</p>
<Checkbox
label="Make profile private"
@ -211,19 +209,16 @@ export default function UserSettings({ toggleSettingsModal }: Props) {
onClick={() => setUser({ ...user, isPrivate: !user.isPrivate })}
/>
<p className="text-gray-500 text-sm mb-3">
<p className="text-gray-500 text-sm">
This will limit who can find and add you to other Collections.
</p>
{user.isPrivate ? (
<div>
<p className="text-sm font-bold text-sky-300 mb-2">
Whitelisted Users
</p>
<p className="text-sm text-sky-500 mb-2">Whitelisted Users</p>
<p className="text-gray-500 text-sm mb-3">
Please provide the Email addresses of the users you wish to grant
visibility to your profile. Separate by comma. Users not included
will be unable to view your profile.
visibility to your profile. Separated by comma.
</p>
<textarea
className="w-full resize-none border rounded-md duration-100 bg-white p-2 outline-none border-sky-100 focus:border-sky-500"

View File

@ -51,7 +51,7 @@ export default function ({ link, count }: Props) {
<div className="flex justify-between items-center gap-5 w-full h-full z-0">
<div className="flex flex-col justify-between">
<div className="flex items-baseline gap-1">
<p className="text-sm text-sky-300 font-bold">{count + 1}.</p>
<p className="text-sm text-sky-400 font-bold">{count + 1}.</p>
<p className="text-lg text-sky-600 font-bold">{link.name}</p>
</div>

View File

@ -7,7 +7,6 @@ import {
faBookmark,
faChartSimple,
} from "@fortawesome/free-solid-svg-icons";
import SidebarItem from "./SidebarItem";
import useTagStore from "@/store/tags";
import Link from "next/link";
import { useRouter } from "next/router";
@ -23,13 +22,13 @@ export default function ({ className }: { className?: string }) {
useEffect(() => {
setActive(router.asPath);
}, [router]);
}, [router, collections]);
return (
<div
className={`bg-gray-100 h-screen w-64 xl:w-80 overflow-y-auto border-solid border-r-sky-100 px-2 border z-20 ${className}`}
>
<p className="p-3 text-sky-500 font-bold text-2xl my-2 leading-4">
<p className="p-2 text-sky-500 font-bold text-2xl my-2 leading-4">
Linkwarden
</p>
@ -38,45 +37,29 @@ export default function ({ className }: { className?: string }) {
<div
className={`${
active === "/dashboard"
? "bg-sky-500"
? "bg-sky-200"
: "hover:bg-slate-200 bg-gray-100"
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex items-center gap-2`}
>
<FontAwesomeIcon
icon={faChartSimple}
className={`w-4 h-4 ${
active === "/dashboard" ? "text-white" : "text-sky-300"
}`}
className={`w-6 h-6 drop-shadow text-sky-500`}
/>
<p
className={`${
active === "/dashboard" ? "text-white" : "text-sky-900"
}`}
>
Dashboard
</p>
<p className="text-sky-600">Dashboard</p>
</div>
</Link>
<Link href="/collections">
<div
className={`${
active === "/collections" ? "bg-sky-500" : "hover:bg-slate-200"
active === "/collections" ? "bg-sky-200" : "hover:bg-slate-200"
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex items-center gap-2`}
>
<FontAwesomeIcon
icon={faBox}
className={`w-4 h-4 ${
active === "/collections" ? "text-white" : "text-sky-300"
}`}
className={`w-6 h-6 drop-shadow text-sky-500`}
/>
<p
className={`${
active === "/collections" ? "text-white" : "text-sky-900"
}`}
>
All Collections
</p>
<p className="text-sky-600">All Collections</p>
</div>
</Link>
@ -84,60 +67,71 @@ export default function ({ className }: { className?: string }) {
<div
className={`${
active === "/links"
? "bg-sky-500"
? "bg-sky-200"
: "hover:bg-slate-200 bg-gray-100"
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex items-center gap-2`}
>
<FontAwesomeIcon
icon={faBookmark}
className={`w-4 h-4 ${
active === "/links" ? "text-white" : "text-sky-300"
}`}
className={`w-6 h-6 drop-shadow text-sky-500`}
/>
<p
className={`${
active === "/links" ? "text-white" : "text-sky-900"
}`}
>
All Links
</p>
<p className="text-sky-600">All Links</p>
</div>
</Link>
</div>
<div className="text-gray-500 mt-5">
<p className="text-sm mb-2 pl-3 font-semibold">Collections</p>
<p className="text-sm mb-2 pl-2 font-semibold">Collections</p>
</div>
<div className="flex flex-col gap-1">
{collections
.sort((a, b) => a.name.localeCompare(b.name))
.map((e, i) => {
return (
<SidebarItem
key={i}
text={e.name}
icon={<FontAwesomeIcon icon={faFolder} />}
iconColor={e.color}
path={`/collections/${e.id}`}
className="capitalize"
<Link key={i} href={`/collections/${e.id}`}>
<div
className={`${
active === `/collections/${e.id}`
? "bg-sky-200"
: "hover:bg-slate-200 bg-gray-100"
} duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
>
<FontAwesomeIcon
icon={faFolder}
className="w-6 h-6 drop-shadow"
style={{ color: e.color }}
/>
<p className="text-sky-600 truncate w-4/6">{e.name}</p>
</div>
</Link>
);
})}
</div>
<div className="text-gray-500 mt-5">
<p className="text-sm mb-2 pl-3 font-semibold">Tags</p>
<p className="text-sm mb-2 pl-2 font-semibold">Tags</p>
</div>
<div className="flex flex-col gap-1">
{tags
.sort((a, b) => a.name.localeCompare(b.name))
.map((e, i) => {
return (
<SidebarItem
key={i}
text={e.name}
icon={<FontAwesomeIcon icon={faHashtag} />}
path={`/tags/${e.id}`}
<Link key={i} href={`/tags/${e.id}`}>
<div
className={`${
active === `/tags/${e.id}`
? "bg-sky-200"
: "hover:bg-slate-200 bg-gray-100"
} duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
>
<FontAwesomeIcon
icon={faHashtag}
className="w-4 h-4 text-sky-500 mt-1"
/>
<p className="text-sky-600 truncate w-4/6">{e.name}</p>
</div>
</Link>
);
})}
</div>

View File

@ -1,49 +0,0 @@
import Link from "next/link";
import React, { ReactElement, useEffect, useState } from "react";
import { useRouter } from "next/router";
interface SidebarItemProps {
text: string;
icon: ReactElement;
path: string;
className?: string;
iconColor?: string;
}
export default function ({
text,
icon,
path,
className,
iconColor,
}: SidebarItemProps) {
const router = useRouter();
const [active, setActive] = useState(false);
useEffect(() => {
if (router.asPath === path) setActive(true);
else setActive(false);
}, [router]);
return (
<Link href={path}>
<div
className={`${
active ? "bg-sky-500" : "hover:bg-slate-200 bg-gray-100"
} duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md ${className}`}
>
{React.cloneElement(icon, {
className: "w-4 h-4",
style: {
color: active ? "white" : iconColor ? iconColor : "#7dd3fc",
},
})}
<p
className={`${active ? "text-white" : "text-sky-900"} truncate w-4/6`}
>
{text}
</p>
</div>
</Link>
);
}

View File

@ -19,7 +19,7 @@ export default function SortLinkDropdown({
const target = e.target as HTMLInputElement;
if (target.id !== "sort-dropdown") toggleSortDropdown();
}}
className="absolute top-8 right-0 shadow-md bg-gray-50 rounded-md p-2 z-10 border border-sky-100 w-48"
className="absolute top-8 right-0 shadow-md bg-gray-50 rounded-md p-2 z-10 w-48"
>
<p className="mb-2 text-sky-900 text-center font-semibold">Sort by</p>
<div className="flex flex-col gap-2">

View File

@ -4,6 +4,7 @@ export default async function (userId: number) {
// remove empty tags
await prisma.tag.deleteMany({
where: {
ownerId: userId,
links: {
none: {},
},

View File

@ -9,3 +9,10 @@ export const prisma =
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
if (process.env.NODE_ENV !== "production")
prisma.$on("query" as any, (e: any) => {
console.log("Query: " + e.query);
console.log("Params: " + e.params);
console.log("\x1b[31m", `Duration: ${e.duration}ms`, "\x1b[0m"); // For benchmarking
});

View File

@ -46,7 +46,6 @@ export default async function (req: NextApiRequest, res: NextApiResponse) {
`data/uploads/avatar/${queryId}.jpg`
);
console.log(filePath);
const file = fs.existsSync(filePath)
? fs.readFileSync(filePath)
: "File not found.";

View File

@ -85,7 +85,7 @@ export default function () {
<div className="flex gap-2">
<FontAwesomeIcon
icon={faBox}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-300"
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500"
/>
<p className="sm:text-4xl text-3xl capitalize bg-gradient-to-tr from-sky-500 to-slate-400 bg-clip-text text-transparent font-bold">
All Collections
@ -218,7 +218,7 @@ export default function () {
activeCollection={{
name: "",
description: "",
color: "#7dd3fc",
color: "#0ea5e9",
isPublic: false,
ownerId: session.data?.user.id as number,
members: [],

View File

@ -39,7 +39,7 @@ export default function () {
<div className="flex gap-2">
<FontAwesomeIcon
icon={faChartSimple}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-300"
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500"
/>
<p className="sm:text-4xl text-3xl capitalize bg-gradient-to-tr from-sky-500 to-slate-400 bg-clip-text text-transparent font-bold">
Dashboard

View File

@ -55,7 +55,7 @@ export default function Links() {
<div className="flex gap-2">
<FontAwesomeIcon
icon={faBookmark}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-300"
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500"
/>
<p className="sm:text-4xl text-3xl capitalize bg-gradient-to-tr from-sky-500 to-slate-400 bg-clip-text text-transparent font-bold">
All Links

View File

@ -1,7 +1,5 @@
import Checkbox from "@/components/Checkbox";
import ClickAwayHandler from "@/components/ClickAwayHandler";
import FilterSearchDropdown from "@/components/FilterSearchDropdown";
import LinkList from "@/components/LinkList";
import RadioButton from "@/components/RadioButton";
import SortLinkDropdown from "@/components/SortLinkDropdown";
import MainLayout from "@/layouts/MainLayout";
import useLinkStore from "@/store/links";
@ -17,8 +15,7 @@ export default function Links() {
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState("Name (A-Z)");
const [sortedLinks, setSortedLinks] = useState(links);
const { searchSettings, toggleCheckbox, setSearchSettings, setSearchQuery } =
useSearchSettingsStore();
const { searchSettings, toggleCheckbox } = useSearchSettingsStore();
const handleSortChange = (event: ChangeEvent<HTMLInputElement>) => {
setSortBy(event.target.value);
@ -93,45 +90,11 @@ export default function Links() {
</div>
{filterDropdown ? (
<ClickAwayHandler
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "filter-dropdown")
setFilterDropdown(false);
}}
className="absolute top-8 right-0 shadow-md bg-gray-50 rounded-md p-2 z-20 border border-sky-100 w-40"
>
<p className="mb-2 text-sky-900 text-center font-semibold">
Filter by
</p>
<div className="flex flex-col gap-2">
<Checkbox
label="Name"
state={searchSettings.filter.name}
onClick={() => toggleCheckbox("name")}
<FilterSearchDropdown
setFilterDropdown={setFilterDropdown}
searchSettings={searchSettings}
toggleCheckbox={toggleCheckbox}
/>
<Checkbox
label="Link"
state={searchSettings.filter.url}
onClick={() => toggleCheckbox("url")}
/>
<Checkbox
label="Title"
state={searchSettings.filter.title}
onClick={() => toggleCheckbox("title")}
/>
<Checkbox
label="Collection"
state={searchSettings.filter.collection}
onClick={() => toggleCheckbox("collection")}
/>
<Checkbox
label="Tags"
state={searchSettings.filter.tags}
onClick={() => toggleCheckbox("tags")}
/>
</div>
</ClickAwayHandler>
) : null}
</div>

146
pages/search/index.tsx Normal file
View File

@ -0,0 +1,146 @@
import FilterSearchDropdown from "@/components/FilterSearchDropdown";
import LinkList from "@/components/LinkList";
import SortLinkDropdown from "@/components/SortLinkDropdown";
import MainLayout from "@/layouts/MainLayout";
import useLinkStore from "@/store/links";
import useSearchSettingsStore from "@/store/search";
import { faFilter, faSearch, faSort } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ChangeEvent, useEffect, useState } from "react";
export default function Links() {
const { links } = useLinkStore();
const [filterDropdown, setFilterDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState("Name (A-Z)");
const [sortedLinks, setSortedLinks] = useState(links);
const { searchSettings, toggleCheckbox } = useSearchSettingsStore();
const handleSortChange = (event: ChangeEvent<HTMLInputElement>) => {
setSortBy(event.target.value);
};
const { name, url, title, collection, tags } = searchSettings.filter;
useEffect(() => {
const linksArray = [
...links.filter((link) => {
const query = searchSettings.query.toLowerCase();
if (
(name && link.name.toLowerCase().includes(query)) ||
(url && link.url.toLowerCase().includes(query)) ||
(title && link.title.toLowerCase().includes(query)) ||
(collection && link.collection.name.toLowerCase().includes(query)) ||
(tags &&
link.tags.some((tag) => tag.name.toLowerCase().includes(query)))
)
return true;
}),
];
if (sortBy === "Name (A-Z)")
setSortedLinks(linksArray.sort((a, b) => a.name.localeCompare(b.name)));
else if (sortBy === "Title (A-Z)")
setSortedLinks(linksArray.sort((a, b) => a.title.localeCompare(b.title)));
else if (sortBy === "Name (Z-A)")
setSortedLinks(linksArray.sort((a, b) => b.name.localeCompare(a.name)));
else if (sortBy === "Title (Z-A)")
setSortedLinks(linksArray.sort((a, b) => b.title.localeCompare(a.title)));
else if (sortBy === "Date (Newest First)")
setSortedLinks(
linksArray.sort(
(a, b) =>
new Date(b.createdAt as string).getTime() -
new Date(a.createdAt as string).getTime()
)
);
else if (sortBy === "Date (Oldest First)")
setSortedLinks(
linksArray.sort(
(a, b) =>
new Date(a.createdAt as string).getTime() -
new Date(b.createdAt as string).getTime()
)
);
}, [searchSettings, links, sortBy]);
return (
<MainLayout>
<div className="p-5 flex flex-col gap-5 w-full">
<div className="flex gap-3 items-center justify-between">
<div className="flex gap-3 items-center mb-5">
<div className="flex gap-2">
<FontAwesomeIcon
icon={faSearch}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500"
/>
<p className="sm:text-4xl text-3xl capitalize bg-gradient-to-tr from-sky-500 to-slate-400 bg-clip-text text-transparent font-bold">
Search Results
</p>
</div>
</div>
<div className="flex gap-3 items-center">
<div className="relative">
<div
onClick={() => setFilterDropdown(!filterDropdown)}
id="filter-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 duration-100 p-1"
>
<FontAwesomeIcon
icon={faFilter}
id="filter-dropdown"
className="w-5 h-5 text-gray-500"
/>
</div>
{filterDropdown ? (
<FilterSearchDropdown
setFilterDropdown={setFilterDropdown}
searchSettings={searchSettings}
toggleCheckbox={toggleCheckbox}
/>
) : null}
</div>
<div className="relative">
<div
onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 duration-100 p-1"
>
<FontAwesomeIcon
icon={faSort}
id="sort-dropdown"
className="w-5 h-5 text-gray-500"
/>
</div>
{sortDropdown ? (
<SortLinkDropdown
handleSortChange={handleSortChange}
sortBy={sortBy}
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
/>
) : null}
</div>
</div>
</div>
{sortedLinks[0] ? (
sortedLinks.map((e, i) => {
return <LinkList key={i} link={e} count={i} />;
})
) : (
<p className="text-sky-900">
Nothing found.{" "}
<span className="text-sky-500 font-bold text-xl" title="Shruggie">
¯\_()_/¯
</span>
</p>
)}
</div>
</MainLayout>
);
}

View File

@ -71,7 +71,7 @@ export default function () {
<div className="flex gap-2">
<FontAwesomeIcon
icon={faHashtag}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-300"
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500"
/>
<p className="sm:text-4xl text-3xl capitalize bg-gradient-to-tr from-sky-500 to-slate-400 bg-clip-text text-transparent font-bold">
{activeTag?.name}

View File

@ -16,7 +16,7 @@ CREATE TABLE "Collection" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"color" TEXT NOT NULL DEFAULT '#7dd3fc',
"color" TEXT NOT NULL DEFAULT '#0ea5e9',
"isPublic" BOOLEAN NOT NULL DEFAULT false,
"ownerId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

View File

@ -24,7 +24,7 @@ model Collection {
id Int @id @default(autoincrement())
name String
description String @default("")
color String @default("#7dd3fc")
color String @default("#0ea5e9")
isPublic Boolean @default(false)
owner User @relation(fields: [ownerId], references: [id])
ownerId Int