better public page [WIP]

This commit is contained in:
daniel31x13 2023-11-16 03:22:16 -05:00
parent 021f7c9481
commit d972ec2dab
10 changed files with 235 additions and 80 deletions

View File

@ -3,7 +3,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCrown } from "@fortawesome/free-solid-svg-icons"; import { faCrown } from "@fortawesome/free-solid-svg-icons";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import ProfilePhoto from "@/components/ProfilePhoto"; import ProfilePhoto from "@/components/ProfilePhoto";
import { toast } from "react-hot-toast";
import getPublicUserData from "@/lib/client/getPublicUserData"; import getPublicUserData from "@/lib/client/getPublicUserData";
type Props = { type Props = {
@ -11,10 +10,6 @@ type Props = {
}; };
export default function ViewTeam({ collection }: Props) { export default function ViewTeam({ collection }: Props) {
const currentURL = new URL(document.URL);
const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`;
const [collectionOwner, setCollectionOwner] = useState({ const [collectionOwner, setCollectionOwner] = useState({
id: null, id: null,
name: "", name: "",

View File

@ -6,12 +6,13 @@ import Dropdown from "@/components/Dropdown";
import ClickAwayHandler from "@/components/ClickAwayHandler"; import ClickAwayHandler from "@/components/ClickAwayHandler";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Search from "@/components/Search"; import SearchBar from "@/components/SearchBar";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import ProfilePhoto from "@/components/ProfilePhoto"; import ProfilePhoto from "@/components/ProfilePhoto";
import useModalStore from "@/store/modals"; import useModalStore from "@/store/modals";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import useWindowDimensions from "@/hooks/useWindowDimensions"; import useWindowDimensions from "@/hooks/useWindowDimensions";
import ToggleDarkMode from "./ToggleDarkMode";
export default function Navbar() { export default function Navbar() {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
@ -56,7 +57,7 @@ export default function Navbar() {
> >
<FontAwesomeIcon icon={faBars} className="w-5 h-5" /> <FontAwesomeIcon icon={faBars} className="w-5 h-5" />
</div> </div>
<Search /> <SearchBar />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
onClick={() => { onClick={() => {
@ -76,6 +77,9 @@ export default function Navbar() {
New Link New Link
</span> </span>
</div> </div>
<ToggleDarkMode className="sm:flex hidden" />
<div className="relative"> <div className="relative">
<div <div
className="flex gap-1 group sm:hover:bg-slate-200 sm:hover:dark:bg-neutral-700 sm:hover:p-1 sm:hover:pr-2 duration-100 h-10 rounded-full items-center w-fit cursor-pointer" className="flex gap-1 group sm:hover:bg-slate-200 sm:hover:dark:bg-neutral-700 sm:hover:p-1 sm:hover:pr-2 duration-100 h-10 rounded-full items-center w-fit cursor-pointer"

View File

@ -0,0 +1,57 @@
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
type Props = {
placeHolder?: string;
className?: string;
};
export default function PublicSearchBar({ placeHolder, className }: Props) {
const router = useRouter();
const routeQuery = router.query.q;
const [searchQuery, setSearchQuery] = useState(
routeQuery ? decodeURIComponent(routeQuery as string) : ""
);
useEffect(() => {
console.log(router);
});
return (
<div className="flex items-center relative group">
<label
htmlFor="search-box"
className="inline-flex w-fit absolute left-2 pointer-events-none rounded-md text-sky-500 dark:text-sky-500"
>
<FontAwesomeIcon icon={faMagnifyingGlass} className="w-4 h-4" />
</label>
<input
id="search-box"
type="text"
placeholder={placeHolder}
value={searchQuery}
onChange={(e) => {
e.target.value.includes("%") &&
toast.error("The search query should not contain '%'.");
setSearchQuery(e.target.value.replace("%", ""));
}}
onKeyDown={(e) =>
e.key === "Enter" &&
router.push(
"/public/collections/" +
router.query.id +
"?q=" +
encodeURIComponent(searchQuery)
)
}
className="border text-sm border-sky-100 bg-white dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 rounded-md pl-7 py-1 pr-1 w-44 sm:w-60 dark:hover:border-neutral-600 md:focus:w-80 hover:border-sky-300 duration-100 outline-none dark:bg-neutral-800"
/>
</div>
);
}

View File

@ -4,24 +4,17 @@ import { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
export default function Search() { export default function SearchBar() {
const router = useRouter(); const router = useRouter();
const routeQuery = router.query.query; const routeQuery = router.query.q;
const [searchQuery, setSearchQuery] = useState( const [searchQuery, setSearchQuery] = useState(
routeQuery ? decodeURIComponent(routeQuery as string) : "" routeQuery ? decodeURIComponent(routeQuery as string) : ""
); );
const [searchBox, setSearchBox] = useState(
router.pathname.startsWith("/search") || false
);
return ( return (
<div <div className="flex items-center relative group">
className="flex items-center relative group"
onClick={() => setSearchBox(true)}
>
<label <label
htmlFor="search-box" htmlFor="search-box"
className="inline-flex w-fit absolute left-2 pointer-events-none rounded-md p-1 text-sky-500 dark:text-sky-500" className="inline-flex w-fit absolute left-2 pointer-events-none rounded-md p-1 text-sky-500 dark:text-sky-500"
@ -43,7 +36,6 @@ export default function Search() {
e.key === "Enter" && e.key === "Enter" &&
router.push("/search?q=" + encodeURIComponent(searchQuery)) router.push("/search?q=" + encodeURIComponent(searchQuery))
} }
autoFocus={searchBox}
className="border border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 rounded-md pl-10 py-2 pr-2 w-44 sm:w-60 dark:hover:border-neutral-600 md:focus:w-80 hover:border-sky-300 duration-100 outline-none dark:bg-neutral-800" className="border border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 rounded-md pl-10 py-2 pr-2 w-44 sm:w-60 dark:hover:border-neutral-600 md:focus:w-80 hover:border-sky-300 duration-100 outline-none dark:bg-neutral-800"
/> />
</div> </div>

View File

@ -2,7 +2,11 @@ import { useTheme } from "next-themes";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons"; import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
export default function ToggleDarkMode() { type Props = {
className?: string;
};
export default function ToggleDarkMode({ className }: Props) {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const handleToggle = () => { const handleToggle = () => {
@ -15,15 +19,13 @@ export default function ToggleDarkMode() {
return ( return (
<div <div
className="flex gap-1 duration-100 h-10 rounded-full items-center w-fit cursor-pointer" className={`cursor-pointer flex select-none border border-sky-600 items-center justify-center dark:bg-neutral-900 bg-white hover:border-sky-500 group duration-100 rounded-full text-white w-10 h-10 ${className}`}
onClick={handleToggle} onClick={handleToggle}
> >
<div className="shadow bg-sky-700 dark:bg-sky-400 flex items-center justify-center rounded-full text-white w-10 h-10 duration-100"> <FontAwesomeIcon
<FontAwesomeIcon icon={theme === "dark" ? faSun : faMoon}
icon={theme === "dark" ? faSun : faMoon} className="w-1/2 h-1/2 text-sky-600 group-hover:text-sky-500"
className="w-1/2 h-1/2" />
/>
</div>
</div> </div>
); );
} }

View File

@ -11,8 +11,6 @@ const getPublicCollectionData = async (
const data = await res.json(); const data = await res.json();
console.log(data);
setData(data.response); setData(data.response);
return data; return data;

View File

@ -22,7 +22,6 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.15", "@headlessui/react": "^1.7.15",
"@mozilla/readability": "^0.4.4", "@mozilla/readability": "^0.4.4",
"@next/font": "13.4.9",
"@prisma/client": "^4.16.2", "@prisma/client": "^4.16.2",
"@stripe/stripe-js": "^1.54.1", "@stripe/stripe-js": "^1.54.1",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
@ -40,6 +39,7 @@
"eslint-config-next": "13.4.9", "eslint-config-next": "13.4.9",
"framer-motion": "^10.16.4", "framer-motion": "^10.16.4",
"jsdom": "^22.1.0", "jsdom": "^22.1.0",
"lottie-web": "^5.12.2",
"micro": "^10.0.1", "micro": "^10.0.1",
"next": "13.4.12", "next": "13.4.12",
"next-auth": "^4.22.1", "next-auth": "^4.22.1",

View File

@ -11,6 +11,16 @@ import useLinkStore from "@/store/links";
import ProfilePhoto from "@/components/ProfilePhoto"; import ProfilePhoto from "@/components/ProfilePhoto";
import useModalStore from "@/store/modals"; import useModalStore from "@/store/modals";
import ModalManagement from "@/components/ModalManagement"; import ModalManagement from "@/components/ModalManagement";
import ToggleDarkMode from "@/components/ToggleDarkMode";
import { useTheme } from "next-themes";
import getPublicUserData from "@/lib/client/getPublicUserData";
import Image from "next/image";
import Link from "next/link";
import PublicSearchBar from "@/components/PublicSearchBar";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFilter, faSort } from "@fortawesome/free-solid-svg-icons";
import FilterSearchDropdown from "@/components/FilterSearchDropdown";
import SortDropdown from "@/components/SortDropdown";
const cardVariants: Variants = { const cardVariants: Variants = {
offscreen: { offscreen: {
@ -28,10 +38,27 @@ const cardVariants: Variants = {
export default function PublicCollections() { export default function PublicCollections() {
const { links } = useLinkStore(); const { links } = useLinkStore();
const { setModal } = useModalStore(); const { modal, setModal } = useModalStore();
useEffect(() => {
modal
? (document.body.style.overflow = "hidden")
: (document.body.style.overflow = "auto");
}, [modal]);
const { theme } = useTheme();
const router = useRouter(); const router = useRouter();
const [collectionOwner, setCollectionOwner] = useState({
id: null,
name: "",
username: "",
image: "",
});
useEffect(() => {}, []);
const [searchFilter, setSearchFilter] = useState({ const [searchFilter, setSearchFilter] = useState({
name: true, name: true,
url: true, url: true,
@ -59,31 +86,34 @@ export default function PublicCollections() {
const [collection, setCollection] = const [collection, setCollection] =
useState<CollectionIncludingMembersAndLinkCount>(); useState<CollectionIncludingMembersAndLinkCount>();
document.body.style.background = "white";
useEffect(() => { useEffect(() => {
if (router.query.id) { if (router.query.id) {
getPublicCollectionData(Number(router.query.id), setCollection); getPublicCollectionData(Number(router.query.id), setCollection);
} }
// document
// .querySelector("body")
// ?.classList.add(
// "bg-gradient-to-br",
// "from-slate-50",
// "to-sky-50",
// "min-h-screen"
// );
}, []); }, []);
useEffect(() => {
const fetchOwner = async () => {
if (collection) {
const owner = await getPublicUserData(collection.ownerId as number);
setCollectionOwner(owner);
}
};
fetchOwner();
}, [collection]);
return collection ? ( return collection ? (
<div <div
className="h-screen" className="h-screen"
style={{ style={{
backgroundImage: `linear-gradient(${collection?.color}30 10%, #f3f4f6 30%, #f9fafb 100%)`, backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
theme === "dark" ? "#262626" : "#f3f4f6"
} 50%, ${theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}} }}
> >
<ModalManagement /> <ModalManagement />
{collection ? ( {collection ? (
<Head> <Head>
<title>{collection.name} | Linkwarden</title> <title>{collection.name} | Linkwarden</title>
@ -96,14 +126,26 @@ export default function PublicCollections() {
) : undefined} ) : undefined}
<div className="max-w-4xl mx-auto p-5 bg"> <div className="max-w-4xl mx-auto p-5 bg">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-4xl text-black font-thin mb-5 capitalize mt-10"> <p className="text-4xl font-thin mb-2 capitalize mt-10">
{collection.name} {collection.name}
</p> </p>
<div className="text-black">[Logo]</div> <div className="flex gap-2 items-center mt-8">
<ToggleDarkMode className="w-8 h-8 flex" />
<Link href="https://linkwarden.app/" target="_blank">
<Image
src={`/icon.png`}
width={551}
height={551}
alt="Linkwarden"
title="Linkwarden"
className="h-8 w-fit mx-auto"
/>
</Link>
</div>
</div> </div>
<div> <div>
<div className={`min-w-[15rem] ${collection.members[1] && "mr-3"}`}> <div className={`min-w-[15rem]`}>
<div <div
onClick={() => onClick={() =>
setModal({ setModal({
@ -115,8 +157,16 @@ export default function PublicCollections() {
defaultIndex: 0, defaultIndex: 0,
}) })
} }
className="hover:opacity-80 duration-100 flex justify-center sm:justify-end items-center w-fit sm:mr-0 sm:ml-auto cursor-pointer" className="hover:opacity-80 duration-100 flex justify-center sm:justify-end items-center w-fit cursor-pointer"
> >
{collectionOwner.id ? (
<ProfilePhoto
src={
collectionOwner.image ? collectionOwner.image : undefined
}
className={`w-8 h-8 border-2`}
/>
) : undefined}
{collection.members {collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number)) .sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => { .map((e, i) => {
@ -124,49 +174,106 @@ export default function PublicCollections() {
<ProfilePhoto <ProfilePhoto
key={i} key={i}
src={e.user.image ? e.user.image : undefined} src={e.user.image ? e.user.image : undefined}
className={`${ className={`w-8 h-8 border-2`}
collection.members[1] && "-mr-3"
} border-[3px]`}
/> />
); );
}) })
.slice(0, 4)} .slice(0, 3)}
{collection?.members.length && {collection?.members.length &&
collection.members.length - 4 > 0 ? ( collection.members.length - 3 > 0 ? (
<div className="h-10 w-10 text-white flex items-center justify-center rounded-full border-[3px] bg-sky-600 dark:bg-sky-600 border-slate-200 dark:border-neutral-700 -mr-3"> <div className="w-8 h-8 text-white flex items-center justify-center rounded-full border-2 bg-sky-600 dark:bg-sky-600 border-slate-200 dark:border-neutral-700">
+{collection?.members?.length - 4} +{collection?.members?.length - 3}
</div> </div>
) : null} ) : null}
<p className="ml-2 text-gray-500 dark:text-gray-300">
By {collectionOwner.name} and {collection.members.length}{" "}
others.
</p>
</div> </div>
</div> </div>
</div> </div>
<p className="mt-2 text-black">{collection.description}</p> <p className="mt-5">{collection.description}</p>
<hr className="mt-5 border-1 border-slate-400" /> <hr className="mt-5 border-1 border-neutral-500" />
<div className="flex flex-col gap-5 my-8"> <div className="flex mb-5 mt-10 flex-col gap-5">
{links <div className="flex justify-between">
?.filter((e) => e.collectionId === Number(router.query.id)) <PublicSearchBar
.map((e, i) => { placeHolder={`Search ${collection._count?.links} Links`}
return ( />
<motion.div
key={i} <div className="flex gap-3 items-center">
initial="offscreen" <div className="relative">
whileInView="onscreen" <div
viewport={{ once: true, amount: 0.8 }} onClick={() => setFilterDropdown(!filterDropdown)}
id="filter-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-neutral-500 hover:bg-opacity-40 duration-100 p-1"
> >
<motion.div variants={cardVariants}> <FontAwesomeIcon
<LinkCard link={e as any} count={i} /> icon={faFilter}
</motion.div> id="filter-dropdown"
</motion.div> className="w-5 h-5 text-gray-500 dark:text-gray-300"
); />
})} </div>
</div>
{/* <p className="text-center text-gray-500"> {filterDropdown ? (
List created with <span className="text-black">Linkwarden.</span> <FilterSearchDropdown
</p> */} setFilterDropdown={setFilterDropdown}
searchFilter={searchFilter}
setSearchFilter={setSearchFilter}
/>
) : null}
</div>
<div className="relative">
<div
onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-neutral-500 hover:bg-opacity-40 duration-100 p-1"
>
<FontAwesomeIcon
icon={faSort}
id="sort-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
</div>
{sortDropdown ? (
<SortDropdown
sortBy={sortBy}
setSort={setSortBy}
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
/>
) : null}
</div>
</div>
</div>
<div className="flex flex-col gap-5">
{links
?.filter((e) => e.collectionId === Number(router.query.id))
.map((e, i) => {
return (
<motion.div
key={i}
initial="offscreen"
whileInView="onscreen"
viewport={{ once: true, amount: 0.8 }}
>
<motion.div variants={cardVariants}>
<LinkCard link={e as any} count={i} />
</motion.div>
</motion.div>
);
})}
</div>
{/* <p className="text-center text-gray-500">
List created with <span className="text-black">Linkwarden.</span>
</p> */}
</div>
</div> </div>
</div> </div>
) : ( ) : (

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@ -919,11 +919,6 @@
dependencies: dependencies:
glob "7.1.7" glob "7.1.7"
"@next/font@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/font/-/font-13.4.9.tgz#5540e69a1a5fbd1113d622a89cdd21c0ab3906c8"
integrity sha512-aR0XEyd1cxqaKuelQFDGwUBYV0wyZfJTNiRoSk1XsECTyMhiSMmCOY7yOPMuPlw+6cctca0GyZXGGFb5EVhiRw==
"@next/swc-darwin-arm64@13.4.12": "@next/swc-darwin-arm64@13.4.12":
version "13.4.12" version "13.4.12"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.12.tgz#326c830b111de8a1a51ac0cbc3bcb157c4c4f92c" resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.12.tgz#326c830b111de8a1a51ac0cbc3bcb157c4c4f92c"
@ -3627,6 +3622,11 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
dependencies: dependencies:
js-tokens "^3.0.0 || ^4.0.0" js-tokens "^3.0.0 || ^4.0.0"
lottie-web@^5.12.2:
version "5.12.2"
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.12.2.tgz#579ca9fe6d3fd9e352571edd3c0be162492f68e5"
integrity sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==
lru-cache@^6.0.0: lru-cache@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"