Merge pull request #160 from linkwarden/dev

Dev
This commit is contained in:
Daniel 2023-09-13 00:15:36 -04:00 committed by GitHub
commit f242d8289a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 969 additions and 738 deletions

View File

@ -22,11 +22,11 @@ EMAIL_SERVER=
# Stripe settings (You don't need these, it's for the cloud instance payments) # Stripe settings (You don't need these, it's for the cloud instance payments)
NEXT_PUBLIC_STRIPE_IS_ACTIVE= NEXT_PUBLIC_STRIPE_IS_ACTIVE=
STRIPE_SECRET_KEY= STRIPE_SECRET_KEY=
PRICE_ID= MONTHLY_PRICE_ID=
YEARLY_PRICE_ID=
NEXT_PUBLIC_TRIAL_PERIOD_DAYS= NEXT_PUBLIC_TRIAL_PERIOD_DAYS=
NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL= NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL=
BASE_URL=http://localhost:3000 BASE_URL=http://localhost:3000
NEXT_PUBLIC_PRICING=
# Docker postgres settings # Docker postgres settings
POSTGRES_PASSWORD= POSTGRES_PASSWORD=

View File

@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img src="./assets/icon.png" width="100px" /> <img src="./assets/logo.png" width="100px" />
<h1>Linkwarden</h1> <h1>Linkwarden</h1>
<a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat-square" alt="Discord"></a> <a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat-square" alt="Discord"></a>
@ -11,7 +11,7 @@
<div align='center'> <div align='center'>
[Website](https://linkwarden.app) | [Getting Started](https://docs.linkwarden.app/getting-started) | [Features](https://github.com/linkwarden/linkwarden#features) | [Roadmap](https://github.com/linkwarden/linkwarden#roadmap) | [Screenshots](https://github.com/linkwarden/linkwarden#screenshots) | [Support ❤](https://github.com/linkwarden/linkwarden#support-) [Website](https://linkwarden.app) | [Getting Started](https://docs.linkwarden.app/getting-started) | [Features](https://github.com/linkwarden/linkwarden#features) | [Roadmap](https://github.com/orgs/linkwarden/projects/1) | [Screenshots](https://github.com/linkwarden/linkwarden#screenshots) | [Support ❤](https://github.com/linkwarden/linkwarden#support-)
</div> </div>
@ -52,13 +52,7 @@ We highly recommend you **not** to use the old version as it is no longer mainta
## Roadmap ## Roadmap
There are _many_ upcoming features, below are only _some_ of the 100% planned ones: Make sure to check out our [public roadmap](https://github.com/orgs/linkwarden/projects/1).
- 🌒 Dark mode.
- 📦 Import/Export your data.
- 🧩 Browser extention.
Also make sure to check out our [public roadmap](https://github.com/orgs/linkwarden/projects/1).
## Docs ## Docs

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -11,9 +11,7 @@ type Props = {
export default function Checkbox({ label, state, className, onClick }: Props) { export default function Checkbox({ label, state, className, onClick }: Props) {
return ( return (
<label <label className={`cursor-pointer flex items-center gap-2 ${className}`}>
className={`cursor-pointer flex items-center gap-2 text-sky-700 ${className}`}
>
<input <input
type="checkbox" type="checkbox"
checked={state} checked={state}
@ -22,13 +20,15 @@ export default function Checkbox({ label, state, className, onClick }: Props) {
/> />
<FontAwesomeIcon <FontAwesomeIcon
icon={faSquareCheck} icon={faSquareCheck}
className="w-5 h-5 text-sky-700 peer-checked:block hidden" className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:block hidden"
/> />
<FontAwesomeIcon <FontAwesomeIcon
icon={faSquare} icon={faSquare}
className="w-5 h-5 text-sky-700 peer-checked:hidden block" className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:hidden block"
/> />
<span className="text-sky-900 rounded select-none">{label}</span> <span className="text-black dark:text-white rounded select-none">
{label}
</span>
</label> </label>
); );
} }

View File

@ -32,24 +32,24 @@ export default function CollectionCard({ collection, className }: Props) {
return ( return (
<div <div
className={`bg-gradient-to-tr from-sky-100 from-10% via-gray-100 via-20% to-white to-100% self-stretch min-h-[12rem] rounded-2xl shadow duration-100 hover:shadow-none group relative ${className}`} className={`bg-gradient-to-tr from-sky-100 dark:from-gray-800 from-10% via-gray-100 via-20% to-white dark:to-neutral-800 to-100% self-stretch min-h-[12rem] rounded-2xl shadow duration-100 hover:shadow-none group relative ${className}`}
> >
<div <div
onClick={() => setExpandDropdown(!expandDropdown)} onClick={() => setExpandDropdown(!expandDropdown)}
id={"expand-dropdown" + collection.id} id={"expand-dropdown" + collection.id}
className="inline-flex absolute top-5 right-5 rounded-md cursor-pointer hover:bg-slate-200 duration-100 p-1" className="inline-flex absolute top-5 right-5 rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faEllipsis} icon={faEllipsis}
id={"expand-dropdown" + collection.id} id={"expand-dropdown" + collection.id}
className="w-5 h-5 text-gray-500" className="w-5 h-5 text-gray-500 dark:text-gray-300"
/> />
</div> </div>
<Link <Link
href={`/collections/${collection.id}`} href={`/collections/${collection.id}`}
className="flex flex-col gap-2 justify-between min-h-[12rem] h-full select-none p-5" className="flex flex-col gap-2 justify-between min-h-[12rem] h-full select-none p-5"
> >
<p className="text-2xl font-bold capitalize text-sky-700 break-words line-clamp-3 w-4/5"> <p className="text-2xl capitalize text-black dark:text-white break-words line-clamp-3 w-4/5">
{collection.name} {collection.name}
</p> </p>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@ -67,17 +67,20 @@ export default function CollectionCard({ collection, className }: Props) {
}) })
.slice(0, 4)} .slice(0, 4)}
{collection.members.length - 4 > 0 ? ( {collection.members.length - 4 > 0 ? (
<div className="h-10 w-10 text-white flex items-center justify-center rounded-full border-[3px] bg-sky-700 border-sky-100 -mr-3"> <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">
+{collection.members.length - 4} +{collection.members.length - 4}
</div> </div>
) : null} ) : null}
</div> </div>
<div className="text-right w-40"> <div className="text-right w-40">
<div className="text-sky-700 font-bold text-sm flex justify-end gap-1 items-center"> <div className="text-black dark:text-white font-bold text-sm flex justify-end gap-1 items-center">
<FontAwesomeIcon icon={faLink} className="w-5 h-5 text-sky-500" /> <FontAwesomeIcon
icon={faLink}
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
{collection._count && collection._count.links} {collection._count && collection._count.links}
</div> </div>
<div className="flex items-center justify-end gap-1 text-gray-600"> <div className="flex items-center justify-end gap-1 text-gray-500 dark:text-gray-300">
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" /> <FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
<p className="font-bold text-xs">{formattedDate}</p> <p className="font-bold text-xs">{formattedDate}</p>
</div> </div>

View File

@ -25,13 +25,13 @@ export default function Dropdown({ onClickOutside, className, items }: Props) {
return ( return (
<ClickAwayHandler <ClickAwayHandler
onClickOutside={onClickOutside} onClickOutside={onClickOutside}
className={`${className} py-1 shadow-md border border-sky-100 bg-gray-50 rounded-md flex flex-col z-20`} className={`${className} py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
> >
{items.map((e, i) => { {items.map((e, i) => {
const inner = e && ( const inner = e && (
<div className="cursor-pointer rounded-md"> <div className="cursor-pointer rounded-md">
<div className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 duration-100"> <div className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 dark:hover:bg-neutral-700 duration-100">
<p className="text-sky-900 select-none">{e.name}</p> <p className="text-black dark:text-white select-none">{e.name}</p>
</div> </div>
</div> </div>
); );

View File

@ -20,9 +20,11 @@ export default function FilterSearchDropdown({
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
if (target.id !== "filter-dropdown") setFilterDropdown(false); if (target.id !== "filter-dropdown") setFilterDropdown(false);
}} }}
className="absolute top-8 right-0 border border-sky-100 shadow-md bg-gray-50 rounded-md p-2 z-20 w-40" className="absolute top-8 right-0 border border-sky-100 dark:border-neutral-700 shadow-md bg-gray-50 dark:bg-neutral-800 rounded-md p-2 z-20 w-40"
> >
<p className="mb-2 text-sky-900 text-center font-semibold">Filter by</p> <p className="mb-2 text-black dark:text-white text-center font-semibold">
Filter by
</p>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Checkbox <Checkbox
label="Name" label="Name"

View File

@ -45,7 +45,8 @@ export default function CollectionSelection({ onChange, defaultValue }: Props) {
return ( return (
<Select <Select
isClearable isClearable
placeholder="Default: Unnamed Collection" className="react-select-container"
classNamePrefix="react-select"
onChange={onChange} onChange={onChange}
options={options} options={options}
styles={styles} styles={styles}

View File

@ -28,6 +28,8 @@ export default function TagSelection({ onChange, defaultValue }: Props) {
return ( return (
<CreatableSelect <CreatableSelect
isClearable isClearable
className="react-select-container"
classNamePrefix="react-select"
onChange={onChange} onChange={onChange}
options={options} options={options}
styles={styles} styles={styles}

View File

@ -106,7 +106,7 @@ export default function LinkCard({ link, count, className }: Props) {
return ( return (
<div <div
className={`bg-gradient-to-tr from-slate-200 from-10% to-gray-50 via-20% shadow hover:shadow-none cursor-pointer duration-100 rounded-2xl relative group ${className}`} className={`h-fit bg-gradient-to-tr from-slate-200 dark:from-neutral-800 from-10% to-gray-50 dark:to-[#303030] via-20% shadow hover:shadow-none cursor-pointer duration-100 rounded-2xl relative group ${className}`}
> >
{(permissions === true || {(permissions === true ||
permissions?.canUpdate || permissions?.canUpdate ||
@ -114,7 +114,7 @@ export default function LinkCard({ link, count, className }: Props) {
<div <div
onClick={() => setExpandDropdown(!expandDropdown)} onClick={() => setExpandDropdown(!expandDropdown)}
id={"expand-dropdown" + link.id} id={"expand-dropdown" + link.id}
className="text-gray-500 inline-flex rounded-md cursor-pointer hover:bg-slate-200 absolute right-5 top-5 z-10 duration-100 p-1" className="text-gray-500 dark:text-gray-300 inline-flex rounded-md cursor-pointer hover:bg-slate-200 dark:hover:bg-neutral-700 absolute right-5 top-5 z-10 duration-100 p-1"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faEllipsis} icon={faEllipsis}
@ -144,7 +144,7 @@ export default function LinkCard({ link, count, className }: Props) {
width={64} width={64}
height={64} height={64}
alt="" alt=""
className="blur-sm absolute w-16 group-hover:opacity-80 duration-100 rounded-md bottom-5 right-5 opacity-60 select-none" className="blur-sm absolute w-16 group-hover:opacity-80 duration-100 rounded-2xl bottom-5 right-5 opacity-60 select-none"
draggable="false" draggable="false"
onError={(e) => { onError={(e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
@ -156,8 +156,10 @@ export default function LinkCard({ link, count, className }: Props) {
<div className="flex justify-between gap-5 w-full h-full z-0"> <div className="flex justify-between gap-5 w-full h-full z-0">
<div className="flex flex-col justify-between w-full"> <div className="flex flex-col justify-between w-full">
<div className="flex items-baseline gap-1"> <div className="flex items-baseline gap-1">
<p className="text-sm text-sky-500 font-bold">{count + 1}.</p> <p className="text-sm text-gray-500 dark:text-gray-300">
<p className="text-lg text-sky-700 font-bold truncate capitalize w-full pr-8"> {count + 1}
</p>
<p className="text-lg text-black dark:text-white truncate capitalize w-full pr-8">
{link.name || link.description} {link.name || link.description}
</p> </p>
</div> </div>
@ -168,16 +170,16 @@ export default function LinkCard({ link, count, className }: Props) {
className="w-4 h-4 mt-1 drop-shadow" className="w-4 h-4 mt-1 drop-shadow"
style={{ color: collection?.color }} style={{ color: collection?.color }}
/> />
<p className="text-sky-900 truncate capitalize"> <p className="text-black dark:text-white truncate capitalize">
{collection?.name} {collection?.name}
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-1 w-full pr-20 text-gray-500"> <div className="flex items-center gap-1 w-full pr-20 text-gray-500 dark:text-gray-300">
<FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" /> <FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" />
<p className="truncate w-full">{shortendURL}</p> <p className="truncate w-full">{shortendURL}</p>
</div> </div>
<div className="flex items-center gap-1 text-gray-500"> <div className="flex items-center gap-1 text-gray-500 dark:text-gray-300">
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" /> <FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
<p>{formattedDate}</p> <p>{formattedDate}</p>
</div> </div>

View File

@ -11,6 +11,7 @@ import SubmitButton from "@/components/SubmitButton";
import { HexColorPicker } from "react-colorful"; import { HexColorPicker } from "react-colorful";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import TextInput from "@/components/TextInput";
type Props = { type Props = {
toggleCollectionModal: Function; toggleCollectionModal: Function;
@ -60,23 +61,23 @@ export default function CollectionInfo({
<div className="flex flex-col gap-3 sm:w-[35rem] w-80"> <div className="flex flex-col gap-3 sm:w-[35rem] w-80">
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-3">
<div className="w-full"> <div className="w-full">
<p className="text-sm text-sky-700 mb-2"> <p className="text-sm text-black dark:text-white mb-2">
Name Name
<RequiredBadge /> <RequiredBadge />
</p> </p>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<input <TextInput
value={collection.name} value={collection.name}
placeholder="e.g. Example Collection"
onChange={(e) => onChange={(e) =>
setCollection({ ...collection, name: e.target.value }) setCollection({ ...collection, name: e.target.value })
} }
type="text"
placeholder="e.g. Example Collection"
className="w-full rounded-md p-3 border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
<div className="color-picker flex justify-between"> <div className="color-picker flex justify-between">
<div className="flex flex-col justify-between items-center w-32"> <div className="flex flex-col justify-between items-center w-32">
<p className="text-sm w-full text-sky-700 mb-2">Icon Color</p> <p className="text-sm w-full text-black dark:text-white mb-2">
Icon Color
</p>
<div style={{ color: collection.color }}> <div style={{ color: collection.color }}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faFolder} icon={faFolder}
@ -84,7 +85,7 @@ export default function CollectionInfo({
/> />
</div> </div>
<div <div
className="py-1 px-2 rounded-md text-xs font-semibold cursor-pointer text-sky-700 hover:bg-slate-200 duration-100" className="py-1 px-2 rounded-md text-xs font-semibold cursor-pointer text-black dark:text-white hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
onClick={() => onClick={() =>
setCollection({ ...collection, color: "#0ea5e9" }) setCollection({ ...collection, color: "#0ea5e9" })
} }
@ -101,9 +102,9 @@ export default function CollectionInfo({
</div> </div>
<div className="w-full"> <div className="w-full">
<p className="text-sm text-sky-700 mb-2">Description</p> <p className="text-sm text-black dark:text-white mb-2">Description</p>
<textarea <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-700" className="w-full h-[11.4rem] resize-none border rounded-md duration-100 bg-gray-50 dark:bg-neutral-950 p-2 outline-none border-sky-100 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600"
placeholder="The purpose of this Collection..." placeholder="The purpose of this Collection..."
value={collection.description} value={collection.description}
onChange={(e) => onChange={(e) =>

View File

@ -9,6 +9,7 @@ import useCollectionStore from "@/store/collections";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import TextInput from "@/components/TextInput";
type Props = { type Props = {
toggleDeleteCollectionModal: Function; toggleDeleteCollectionModal: Function;
@ -50,7 +51,7 @@ export default function DeleteCollection({
<p className="text-red-500 font-bold text-center">Warning!</p> <p className="text-red-500 font-bold text-center">Warning!</p>
<div className="max-h-[20rem] overflow-y-auto"> <div className="max-h-[20rem] overflow-y-auto">
<div className="text-gray-500"> <div className="text-black dark:text-white">
<p> <p>
Please note that deleting the collection will permanently remove Please note that deleting the collection will permanently remove
all its contents, including the following: all its contents, including the following:
@ -81,25 +82,24 @@ export default function DeleteCollection({
</div> </div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<p className="text-sky-900 select-none text-center"> <p className="text-black dark:text-white select-none text-center">
To confirm, type &quot; To confirm, type &quot;
<span className="font-bold text-sky-700">{collection.name}</span> <span className="font-bold">{collection.name}</span>
&quot; in the box below: &quot; in the box below:
</p> </p>
<input <TextInput
autoFocus autoFocus={true}
value={inputField} value={inputField}
onChange={(e) => setInputField(e.target.value)} onChange={(e) => setInputField(e.target.value)}
type="text"
placeholder={`Type "${collection.name}" Here.`} placeholder={`Type "${collection.name}" Here.`}
className="w-72 sm:w-96 rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100" className="w-3/4 mx-auto"
/> />
</div> </div>
</> </>
) : ( ) : (
<p className="text-gray-500"> <p className="text-black dark:text-white">
Click the button below to leave the current collection: Click the button below to leave the current collection.
</p> </p>
)} )}
@ -107,9 +107,9 @@ export default function DeleteCollection({
className={`mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none font-bold duration-100 ${ className={`mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none font-bold duration-100 ${
permissions === true permissions === true
? inputField === collection.name ? inputField === collection.name
? "bg-red-500 hover:bg-red-400 cursor-pointer" ? "bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer"
: "cursor-not-allowed bg-red-300" : "cursor-not-allowed bg-red-300 dark:bg-red-900"
: "bg-red-500 hover:bg-red-400 cursor-pointer" : "bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer"
}`} }`}
onClick={submit} onClick={submit}
> >

View File

@ -17,6 +17,7 @@ import ProfilePhoto from "@/components/ProfilePhoto";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import getPublicUserData from "@/lib/client/getPublicUserData"; import getPublicUserData from "@/lib/client/getPublicUserData";
import TextInput from "@/components/TextInput";
type Props = { type Props = {
toggleCollectionModal: Function; toggleCollectionModal: Function;
@ -117,7 +118,7 @@ export default function TeamManagement({
<div className="flex flex-col gap-3 sm:w-[35rem] w-80"> <div className="flex flex-col gap-3 sm:w-[35rem] w-80">
{permissions === true && ( {permissions === true && (
<> <>
<p className="text-sm text-sky-700">Make Public</p> <p className="text-sm text-black dark:text-white">Make Public</p>
<Checkbox <Checkbox
label="Make this a public collection." label="Make this a public collection."
@ -127,7 +128,7 @@ export default function TeamManagement({
} }
/> />
<p className="text-gray-500 text-sm"> <p className="text-gray-500 dark:text-gray-300 text-sm">
This will let <b>Anyone</b> to view this collection. This will let <b>Anyone</b> to view this collection.
</p> </p>
</> </>
@ -135,7 +136,7 @@ export default function TeamManagement({
{collection.isPublic ? ( {collection.isPublic ? (
<div> <div>
<p className="text-sm text-sky-700 mb-2"> <p className="text-sm text-black dark:text-white mb-2">
Public Link (Click to copy) Public Link (Click to copy)
</p> </p>
<div <div
@ -148,7 +149,7 @@ export default function TeamManagement({
console.log(err); console.log(err);
} }
}} }}
className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-3 border-sky-100 border-solid border outline-none hover:border-sky-700 duration-100 cursor-text" className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 dark:bg-neutral-950 border-sky-100 dark:border-neutral-700 border-solid border outline-none hover:border-sky-300 dark:hover:border-sky-600 duration-100 cursor-text"
> >
{publicCollectionURL} {publicCollectionURL}
</div> </div>
@ -159,11 +160,14 @@ export default function TeamManagement({
{permissions === true && ( {permissions === true && (
<> <>
<p className="text-sm text-sky-700">Member Management</p> <p className="text-sm text-black dark:text-white">
Member Management
</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <TextInput
value={member.user.username || ""} value={member.user.username || ""}
placeholder="Username (without the '@')"
onChange={(e) => { onChange={(e) => {
setMember({ setMember({
...member, ...member,
@ -179,9 +183,6 @@ export default function TeamManagement({
setMemberState setMemberState
) )
} }
type="text"
placeholder="Username (without the '@')"
className="w-full rounded-md p-3 border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
<div <div
@ -193,7 +194,7 @@ export default function TeamManagement({
setMemberState setMemberState
) )
} }
className="flex items-center justify-center bg-sky-700 hover:bg-sky-600 duration-100 text-white w-12 h-12 p-3 rounded-md cursor-pointer" className="flex items-center justify-center bg-sky-700 hover:bg-sky-600 duration-100 text-white w-10 h-10 p-2 rounded-md cursor-pointer"
> >
<FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" /> <FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" />
</div> </div>
@ -203,7 +204,7 @@ export default function TeamManagement({
{collection?.members[0]?.user && ( {collection?.members[0]?.user && (
<> <>
<p className="text-center text-gray-500 text-xs sm:text-sm"> <p className="text-center text-gray-500 dark:text-gray-300 text-xs sm:text-sm">
(All Members have <b>Read</b> access to this collection.) (All Members have <b>Read</b> access to this collection.)
</p> </p>
<div className="flex flex-col gap-3 rounded-md"> <div className="flex flex-col gap-3 rounded-md">
@ -213,12 +214,12 @@ export default function TeamManagement({
return ( return (
<div <div
key={i} key={i}
className="relative border p-2 rounded-md border-sky-100 flex flex-col sm:flex-row sm:items-center gap-2 justify-between" className="relative border p-2 rounded-md border-sky-100 dark:border-neutral-700 flex flex-col sm:flex-row sm:items-center gap-2 justify-between"
> >
{permissions === true && ( {permissions === true && (
<FontAwesomeIcon <FontAwesomeIcon
icon={faClose} icon={faClose}
className="absolute right-2 top-2 text-gray-500 h-4 hover:text-red-500 duration-100 cursor-pointer" className="absolute right-2 top-2 text-gray-500 dark:text-gray-300 h-4 hover:text-red-500 dark:hover:text-red-500 duration-100 cursor-pointer"
title="Remove Member" title="Remove Member"
onClick={() => { onClick={() => {
const updatedMembers = collection.members.filter( const updatedMembers = collection.members.filter(
@ -239,23 +240,25 @@ export default function TeamManagement({
className="border-[3px]" className="border-[3px]"
/> />
<div> <div>
<p className="text-sm font-bold text-sky-700"> <p className="text-sm font-bold text-black dark:text-white">
{e.user.name} {e.user.name}
</p> </p>
<p className="text-sky-900">@{e.user.username}</p> <p className="text-gray-500 dark:text-gray-300">
@{e.user.username}
</p>
</div> </div>
</div> </div>
<div className="flex sm:block items-center gap-5 min-w-[10rem]"> <div className="flex sm:block items-center justify-between gap-5 min-w-[10rem]">
<div> <div>
<p <p
className={`font-bold text-sm text-sky-700 ${ className={`font-bold text-sm text-black dark:text-white ${
permissions === true ? "" : "mb-2" permissions === true ? "" : "mb-2"
}`} }`}
> >
Permissions Permissions
</p> </p>
{permissions === true && ( {permissions === true && (
<p className="text-xs text-gray-500 mb-2"> <p className="text-xs text-gray-500 dark:text-gray-300 mb-2">
(Click to toggle.) (Click to toggle.)
</p> </p>
)} )}
@ -265,7 +268,7 @@ export default function TeamManagement({
!e.canCreate && !e.canCreate &&
!e.canUpdate && !e.canUpdate &&
!e.canDelete ? ( !e.canDelete ? (
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500 dark:text-gray-300">
Has no permissions. Has no permissions.
</p> </p>
) : ( ) : (
@ -305,11 +308,11 @@ export default function TeamManagement({
}} }}
/> />
<span <span
className={`text-sky-900 peer-checked:bg-sky-700 text-sm ${ className={`text-black dark:text-white peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
permissions === true permissions === true
? "hover:bg-slate-200 duration-75" ? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-75"
: "" : ""
} peer-checked:text-white rounded p-1 select-none`} } rounded p-1 select-none`}
> >
Create Create
</span> </span>
@ -350,11 +353,11 @@ export default function TeamManagement({
}} }}
/> />
<span <span
className={`text-sky-900 peer-checked:bg-sky-700 text-sm ${ className={`text-black dark:text-white peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
permissions === true permissions === true
? "hover:bg-slate-200 duration-75" ? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-75"
: "" : ""
} peer-checked:text-white rounded p-1 select-none`} } rounded p-1 select-none`}
> >
Update Update
</span> </span>
@ -395,11 +398,11 @@ export default function TeamManagement({
}} }}
/> />
<span <span
className={`text-sky-900 peer-checked:bg-sky-700 text-sm ${ className={`text-black dark:text-white peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
permissions === true permissions === true
? "hover:bg-slate-200 duration-75" ? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-75"
: "" : ""
} peer-checked:text-white rounded p-1 select-none`} } rounded p-1 select-none`}
> >
Delete Delete
</span> </span>
@ -415,7 +418,7 @@ export default function TeamManagement({
)} )}
<div <div
className="relative border px-2 rounded-md border-sky-100 flex min-h-[7rem] sm:min-h-[5rem] gap-2 justify-between" className="relative border px-2 rounded-md border-sky-100 dark:border-neutral-700 flex min-h-[7rem] sm:min-h-[5rem] gap-2 justify-between"
title={`'@${collectionOwner.username}' is the owner of this collection.`} title={`'@${collectionOwner.username}' is the owner of this collection.`}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -425,7 +428,7 @@ export default function TeamManagement({
/> />
<div> <div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<p className="text-sm font-bold text-sky-700"> <p className="text-sm font-bold text-black dark:text-white">
{collectionOwner.name} {collectionOwner.name}
</p> </p>
<FontAwesomeIcon <FontAwesomeIcon
@ -433,13 +436,15 @@ export default function TeamManagement({
className="w-3 h-3 text-yellow-500" className="w-3 h-3 text-yellow-500"
/> />
</div> </div>
<p className="text-sky-900">@{collectionOwner.username}</p> <p className="text-gray-500 dark:text-gray-300">
@{collectionOwner.username}
</p>
</div> </div>
</div> </div>
<div className="flex flex-col justify-center min-w-[10rem]"> <div className="flex flex-col justify-center min-w-[10rem] text-black dark:text-white">
<p className={`font-bold text-sm text-sky-700`}>Permissions</p> <p className={`font-bold text-sm`}>Permissions</p>
<p className="text-sky-700">Full Access (Owner)</p> <p>Full Access (Owner)</p>
</div> </div>
</div> </div>

View File

@ -46,17 +46,19 @@ export default function CollectionModal({
<div className={className}> <div className={className}>
<Tab.Group defaultIndex={defaultIndex}> <Tab.Group defaultIndex={defaultIndex}>
{method === "CREATE" && ( {method === "CREATE" && (
<p className="text-xl text-sky-700 text-center">New Collection</p> <p className="text-xl text-black dark:text-white text-center">
New Collection
</p>
)} )}
<Tab.List className="flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-sky-700"> <Tab.List className="flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-black dark:text-white">
{method === "UPDATE" && ( {method === "UPDATE" && (
<> <>
{isOwner && ( {isOwner && (
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" ? "px-2 py-1 bg-sky-200 dark:bg-sky-800 dark:text-white duration-100 rounded-md outline-none"
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" : "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 hover:dark:text-white rounded-md duration-100 outline-none"
} }
> >
Collection Info Collection Info
@ -65,8 +67,8 @@ export default function CollectionModal({
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" ? "px-2 py-1 bg-sky-200 dark:bg-sky-800 dark:text-white duration-100 rounded-md outline-none"
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" : "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 hover:dark:text-white rounded-md duration-100 outline-none"
} }
> >
{isOwner ? "Share & Collaborate" : "View Team"} {isOwner ? "Share & Collaborate" : "View Team"}
@ -74,8 +76,8 @@ export default function CollectionModal({
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" ? "px-2 py-1 bg-sky-200 dark:bg-sky-800 dark:text-white duration-100 rounded-md outline-none"
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" : "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 hover:dark:text-white rounded-md duration-100 outline-none"
} }
> >
{isOwner ? "Delete Collection" : "Leave Collection"} {isOwner ? "Delete Collection" : "Leave Collection"}

View File

@ -7,11 +7,13 @@ import useLinkStore from "@/store/links";
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import RequiredBadge from "../../RequiredBadge"; import RequiredBadge from "../../RequiredBadge";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import useCollectionStore from "@/store/collections"; // import useCollectionStore from "@/store/collections";
import { useRouter } from "next/router"; // import { useRouter } from "next/router";
import SubmitButton from "../../SubmitButton"; import SubmitButton from "../../SubmitButton";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import Link from "next/link"; import Link from "next/link";
import TextInput from "@/components/TextInput";
import unescapeString from "@/lib/client/unescapeString";
type Props = type Props =
| { | {
@ -32,6 +34,10 @@ export default function AddOrEditLink({
}: Props) { }: Props) {
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
const [optionsExpanded, setOptionsExpanded] = useState(
method === "UPDATE" ? true : false
);
const { data } = useSession(); const { data } = useSession();
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>( const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>(
@ -49,27 +55,27 @@ export default function AddOrEditLink({
const { updateLink, addLink } = useLinkStore(); const { updateLink, addLink } = useLinkStore();
const router = useRouter(); // const router = useRouter();
const { collections } = useCollectionStore(); // const { collections } = useCollectionStore();
useEffect(() => { // useEffect(() => {
if (router.query.id) { // if (router.query.id) {
const currentCollection = collections.find( // const currentCollection = collections.find(
(e) => e.id == Number(router.query.id) // (e) => e.id == Number(router.query.id)
); // );
if (currentCollection && currentCollection.ownerId) // if (currentCollection && currentCollection.ownerId)
setLink({ // setLink({
...link, // ...link,
collection: { // collection: {
id: currentCollection.id, // id: currentCollection.id,
name: currentCollection.name, // name: currentCollection.name,
ownerId: currentCollection.ownerId, // ownerId: currentCollection.ownerId,
}, // },
}); // });
} // }
}, []); // }, []);
const setTags = (e: any) => { const setTags = (e: any) => {
const tagNames = e.map((e: any) => { const tagNames = e.map((e: any) => {
@ -115,7 +121,7 @@ export default function AddOrEditLink({
<div className="flex flex-col gap-3 sm:w-[35rem] w-80"> <div className="flex flex-col gap-3 sm:w-[35rem] w-80">
{method === "UPDATE" ? ( {method === "UPDATE" ? (
<p <p
className="text-gray-500 my-2 text-center truncate w-full" className="text-gray-500 dark:text-gray-300 text-center truncate w-full"
title={link.url} title={link.url}
> >
<Link href={link.url} target="_blank" className="font-bold"> <Link href={link.url} target="_blank" className="font-bold">
@ -125,43 +131,81 @@ export default function AddOrEditLink({
) : null} ) : null}
{method === "CREATE" ? ( {method === "CREATE" ? (
<div> <div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
<p className="text-sm text-sky-700 mb-2 font-bold"> <div className="sm:col-span-3 col-span-5">
<p className="text-sm text-black dark:text-white mb-2 font-bold">
Address (URL) Address (URL)
<RequiredBadge /> <RequiredBadge />
</p> </p>
<input <TextInput
value={link.url} value={link.url}
onChange={(e) => setLink({ ...link, url: e.target.value })} onChange={(e) => setLink({ ...link, url: e.target.value })}
type="text"
placeholder="e.g. http://example.com/" placeholder="e.g. http://example.com/"
className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
) : null} <div className="sm:col-span-2 col-span-5">
<hr /> <p className="text-sm text-black dark:text-white mb-2">
<div className="grid sm:grid-cols-2 gap-3"> Collection
<div> </p>
<p className="text-sm text-sky-700 mb-2">Collection</p>
<CollectionSelection <CollectionSelection
onChange={setCollection} onChange={setCollection}
// defaultValue={{ // defaultValue={{
// label: link.collection.name, // label: link.collection.name,
// value: link.collection.id, // value: link.collection.id,
// }} // }}
defaultValue={
link.collection.id
? {
value: link.collection.id,
label: link.collection.name,
}
: {
value: null as unknown as number,
label: "Unorganized",
}
}
/>
</div>
</div>
) : null}
{optionsExpanded ? (
<div>
<hr className="mb-3 border border-sky-100 dark:border-neutral-700" />
<div className="grid sm:grid-cols-2 gap-3">
<div className={`${method === "UPDATE" ? "sm:col-span-2" : ""}`}>
<p className="text-sm text-black dark:text-white mb-2">Name</p>
<TextInput
value={link.name}
onChange={(e) => setLink({ ...link, name: e.target.value })}
placeholder="e.g. Example Link"
/>
</div>
{method === "UPDATE" ? (
<div>
<p className="text-sm text-black dark:text-white mb-2">
Collection
</p>
<CollectionSelection
onChange={setCollection}
defaultValue={ defaultValue={
link.collection.name && link.collection.id link.collection.name && link.collection.id
? { ? {
value: link.collection.id, value: link.collection.id,
label: link.collection.name, label: link.collection.name,
} }
: undefined : {
value: null as unknown as number,
label: "Unorganized",
}
} }
/> />
</div> </div>
) : undefined}
<div> <div>
<p className="text-sm text-sky-700 mb-2">Tags</p> <p className="text-sm text-black dark:text-white mb-2">Tags</p>
<TagSelection <TagSelection
onChange={setTags} onChange={setTags}
defaultValue={link.tags.map((e) => { defaultValue={link.tags.map((e) => {
@ -171,38 +215,44 @@ export default function AddOrEditLink({
</div> </div>
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<p className="text-sm text-sky-700 mb-2">Name</p> <p className="text-sm text-black dark:text-white mb-2">
<input Description
value={link.name} </p>
onChange={(e) => setLink({ ...link, name: e.target.value })}
type="text"
placeholder="e.g. Example Link"
className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/>
</div>
<div className="sm:col-span-2">
<p className="text-sm text-sky-700 mb-2">Description</p>
<textarea <textarea
value={link.description} value={unescapeString(link.description) as string}
onChange={(e) => setLink({ ...link, description: e.target.value })} onChange={(e) =>
setLink({ ...link, description: e.target.value })
}
placeholder={ placeholder={
method === "CREATE" method === "CREATE"
? "Will be auto generated if nothing is provided." ? "Will be auto generated if nothing is provided."
: "" : ""
} }
className="resize-none w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100" className="resize-none w-full rounded-md p-2 border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100 dark:bg-neutral-950"
/> />
</div> </div>
</div> </div>
</div>
) : undefined}
<div className="flex justify-between items-center mt-2">
<div
onClick={() => setOptionsExpanded(!optionsExpanded)}
className={`${
method === "UPDATE" ? "hidden" : ""
} rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 py-1 px-2 w-fit text-sm`}
>
{optionsExpanded ? "Hide" : "More"} Options
</div>
<SubmitButton <SubmitButton
onClick={submit} onClick={submit}
label={method === "CREATE" ? "Add" : "Save"} label={method === "CREATE" ? "Add" : "Save"}
icon={method === "CREATE" ? faPlus : faPenToSquare} icon={method === "CREATE" ? faPlus : faPenToSquare}
loading={submitLoader} loading={submitLoader}
className={`mx-auto mt-2`} className={`${method === "CREATE" ? "" : "mx-auto"}`}
/> />
</div> </div>
</div>
); );
} }

View File

@ -20,12 +20,17 @@ import {
faFilePdf, faFilePdf,
} from "@fortawesome/free-regular-svg-icons"; } from "@fortawesome/free-regular-svg-icons";
import isValidUrl from "@/lib/client/isValidUrl"; import isValidUrl from "@/lib/client/isValidUrl";
import { useTheme } from "next-themes";
import unescapeString from "@/lib/client/unescapeString";
type Props = { type Props = {
link: LinkIncludingShortenedCollectionAndTags; link: LinkIncludingShortenedCollectionAndTags;
isOwnerOrMod: boolean;
}; };
export default function LinkDetails({ link }: Props) { export default function LinkDetails({ link, isOwnerOrMod }: Props) {
const { theme, setTheme } = useTheme();
const [imageError, setImageError] = useState<boolean>(false); const [imageError, setImageError] = useState<boolean>(false);
const formattedDate = new Date(link.createdAt as string).toLocaleString( const formattedDate = new Date(link.createdAt as string).toLocaleString(
"en-US", "en-US",
@ -93,7 +98,7 @@ export default function LinkDetails({ link }: Props) {
colorPalette[3][2] colorPalette[3][2]
)})`; )})`;
} }
}, [colorPalette]); }, [colorPalette, theme]);
const handleDownload = (format: "png" | "pdf") => { const handleDownload = (format: "png" | "pdf") => {
const path = `/api/archives/${link.collection.id}/${link.id}.${format}`; const path = `/api/archives/${link.collection.id}/${link.id}.${format}`;
@ -115,7 +120,11 @@ export default function LinkDetails({ link }: Props) {
}; };
return ( return (
<div className="flex flex-col gap-3 sm:w-[35rem] w-80"> <div
className={`flex flex-col gap-3 sm:w-[35rem] w-80 ${
isOwnerOrMod ? "" : "mt-12"
} ${theme === "dark" ? "banner-dark-mode" : "banner-light-mode"}`}
>
{!imageError && ( {!imageError && (
<div id="link-banner" className="link-banner h-32 -mx-5 -mt-5 relative"> <div id="link-banner" className="link-banner h-32 -mx-5 -mt-5 relative">
<div id="link-banner-inner" className="link-banner-inner"></div> <div id="link-banner-inner" className="link-banner-inner"></div>
@ -131,7 +140,7 @@ export default function LinkDetails({ link }: Props) {
height={42} height={42}
alt="" alt=""
id={"favicon-" + link.id} id={"favicon-" + link.id}
className="select-none mt-2 rounded-md shadow border-[3px] border-white bg-white aspect-square" className="select-none mt-2 w-10 rounded-md shadow border-[3px] border-white dark:border-neutral-900 bg-white dark:bg-neutral-900 aspect-square"
draggable="false" draggable="false"
onLoad={(e) => { onLoad={(e) => {
try { try {
@ -150,15 +159,17 @@ export default function LinkDetails({ link }: Props) {
}} }}
/> />
)} )}
<div className="flex flex-col min-h-[3rem] justify-end drop-shadow"> <div className="flex w-full flex-col min-h-[3rem] justify-center drop-shadow">
<p className="text-2xl text-sky-700 capitalize break-words hyphens-auto"> <p className="text-2xl text-black dark:text-white capitalize break-words hyphens-auto">
{link.name} {unescapeString(link.name)}
</p> </p>
<Link <Link
href={link.url} href={link.url}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-sm text-gray-500 break-all hover:underline cursor-pointer w-fit" className={`${
link.name ? "text-sm" : "text-xl"
} text-gray-500 dark:text-gray-300 break-all hover:underline cursor-pointer w-fit`}
> >
{url ? url.host : link.url} {url ? url.host : link.url}
</Link> </Link>
@ -176,7 +187,7 @@ export default function LinkDetails({ link }: Props) {
/> />
<p <p
title={collection?.name} title={collection?.name}
className="text-sky-900 text-lg truncate max-w-[12rem]" className="text-black dark:text-white text-lg truncate max-w-[12rem]"
> >
{collection?.name} {collection?.name}
</p> </p>
@ -185,7 +196,7 @@ export default function LinkDetails({ link }: Props) {
<Link key={i} href={`/tags/${e.id}`} className="z-10"> <Link key={i} href={`/tags/${e.id}`} className="z-10">
<p <p
title={e.name} title={e.name}
className="px-2 py-1 bg-sky-200 text-sky-700 text-xs rounded-3xl cursor-pointer hover:opacity-60 duration-100 truncate max-w-[19rem]" className="px-2 py-1 bg-sky-200 text-black dark:text-white dark:bg-sky-900 text-xs rounded-3xl cursor-pointer hover:opacity-60 duration-100 truncate max-w-[19rem]"
> >
{e.name} {e.name}
</p> </p>
@ -194,19 +205,19 @@ export default function LinkDetails({ link }: Props) {
</div> </div>
{link.description && ( {link.description && (
<> <>
<div className="text-gray-500 max-h-[20rem] my-3 rounded-md overflow-y-auto hyphens-auto"> <div className="text-black dark:text-white max-h-[20rem] my-3 rounded-md overflow-y-auto hyphens-auto">
{link.description} {unescapeString(link.description)}
</div> </div>
</> </>
)} )}
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-1 text-gray-500"> <div className="flex items-center gap-1 text-gray-500 dark:text-gray-300">
<FontAwesomeIcon icon={faBoxArchive} className="w-4 h-4" /> <FontAwesomeIcon icon={faBoxArchive} className="w-4 h-4" />
<p className=" text-gray-500">Archived Formats:</p> <p>Archived Formats:</p>
</div> </div>
<div <div
className="flex items-center gap-1 text-gray-500" className="flex items-center gap-1 text-gray-500 dark:text-gray-300"
title={"Created at: " + formattedDate} title={"Created at: " + formattedDate}
> >
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" /> <FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
@ -214,69 +225,69 @@ export default function LinkDetails({ link }: Props) {
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex justify-between items-center p-2 border border-sky-100 rounded-md"> <div className="flex justify-between items-center p-2 border border-sky-100 dark:border-neutral-700 rounded-md">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<div className="text-white bg-sky-300 p-2 rounded-md"> <div className="text-white bg-sky-300 dark:bg-sky-600 p-2 rounded-md">
<FontAwesomeIcon icon={faFileImage} className="w-6 h-6" /> <FontAwesomeIcon icon={faFileImage} className="w-6 h-6" />
</div> </div>
<p className="text-gray-500">Screenshot</p> <p className="text-black dark:text-white">Screenshot</p>
</div> </div>
<div className="flex text-sky-500 gap-1"> <div className="flex text-black dark:text-white gap-1">
<Link <Link
href={`/api/archives/${link.collectionId}/${link.id}.png`} href={`/api/archives/${link.collectionId}/${link.id}.png`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="cursor-pointer hover:bg-slate-200 duration-100 p-2 rounded-md" className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowUpRightFromSquare} icon={faArrowUpRightFromSquare}
className="w-5 h-5" className="w-5 h-5 text-sky-500 dark:text-sky-500"
/> />
</Link> </Link>
<div <div
onClick={() => handleDownload("png")} onClick={() => handleDownload("png")}
className="cursor-pointer hover:bg-slate-200 duration-100 p-2 rounded-md" className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faCloudArrowDown} icon={faCloudArrowDown}
className="w-5 h-5 cursor-pointer" className="w-5 h-5 cursor-pointer text-sky-500 dark:text-sky-500"
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-between items-center p-2 border border-sky-100 rounded-md"> <div className="flex justify-between items-center p-2 border border-sky-100 dark:border-neutral-700 rounded-md">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<div className="text-white bg-sky-300 p-2 rounded-md"> <div className="text-white bg-sky-300 dark:bg-sky-600 p-2 rounded-md">
<FontAwesomeIcon icon={faFilePdf} className="w-6 h-6" /> <FontAwesomeIcon icon={faFilePdf} className="w-6 h-6" />
</div> </div>
<p className="text-gray-500">PDF</p> <p className="text-black dark:text-white">PDF</p>
</div> </div>
<div className="flex text-sky-500 gap-1"> <div className="flex text-black dark:text-white gap-1">
<Link <Link
href={`/api/archives/${link.collectionId}/${link.id}.pdf`} href={`/api/archives/${link.collectionId}/${link.id}.pdf`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="cursor-pointer hover:bg-slate-200 duration-100 p-2 rounded-md" className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowUpRightFromSquare} icon={faArrowUpRightFromSquare}
className="w-5 h-5" className="w-5 h-5 text-sky-500 dark:text-sky-500"
/> />
</Link> </Link>
<div <div
onClick={() => handleDownload("pdf")} onClick={() => handleDownload("pdf")}
className="cursor-pointer hover:bg-slate-200 duration-100 p-2 rounded-md" className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faCloudArrowDown} icon={faCloudArrowDown}
className="w-5 h-5 cursor-pointer" className="w-5 h-5 cursor-pointer text-sky-500 dark:text-sky-500"
/> />
</div> </div>
</div> </div>

View File

@ -33,20 +33,18 @@ export default function LinkModal({
<div className={className}> <div className={className}>
<Tab.Group defaultIndex={defaultIndex}> <Tab.Group defaultIndex={defaultIndex}>
{method === "CREATE" && ( {method === "CREATE" && (
<p className="text-xl text-sky-700 text-center">New Link</p> <p className="text-xl text-black dark:text-white text-center">
New Link
</p>
)} )}
<Tab.List <Tab.List className="flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-black dark:text-white">
className={`flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-sky-700 ${
isOwnerOrMod ? "" : "pb-8"
}`}
>
{method === "UPDATE" && isOwnerOrMod && ( {method === "UPDATE" && isOwnerOrMod && (
<> <>
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" ? "px-2 py-1 bg-sky-200 dark:bg-sky-800 duration-100 rounded-md outline-none"
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" : "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 rounded-md duration-100 outline-none"
} }
> >
Link Details Link Details
@ -54,8 +52,8 @@ export default function LinkModal({
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" ? "px-2 py-1 bg-sky-200 dark:bg-sky-800 duration-100 rounded-md outline-none"
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" : "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 rounded-md duration-100 outline-none"
} }
> >
Edit Link Edit Link
@ -66,7 +64,7 @@ export default function LinkModal({
<Tab.Panels> <Tab.Panels>
{activeLink && method === "UPDATE" && ( {activeLink && method === "UPDATE" && (
<Tab.Panel> <Tab.Panel>
<LinkDetails link={activeLink} /> <LinkDetails link={activeLink} isOwnerOrMod={isOwnerOrMod} />
</Tab.Panel> </Tab.Panel>
)} )}

View File

@ -17,8 +17,8 @@ export default function PaymentPortal() {
return ( return (
<div className="mx-auto sm:w-[35rem] w-80"> <div className="mx-auto sm:w-[35rem] w-80">
<div className="max-w-[25rem] w-full mx-auto flex flex-col gap-3 justify-between"> <div className=" w-full mx-auto flex flex-col gap-3 justify-between">
<p className="text-md text-gray-500"> <p className="text-md text-black dark:text-white">
To manage/cancel your subsciption, visit the billing portal. To manage/cancel your subsciption, visit the billing portal.
</p> </p>
@ -30,10 +30,13 @@ export default function PaymentPortal() {
className="mx-auto mt-2" className="mx-auto mt-2"
/> />
<p className="text-md text-gray-500"> <p className="text-md text-black dark:text-white">
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 className="font-semibold" href="mailto:support@linkwarden.app"> <a
className="font-semibold underline"
href="mailto:support@linkwarden.app"
>
support@linkwarden.app support@linkwarden.app
</a> </a>
</p> </p>

View File

@ -5,6 +5,7 @@ import { signOut, useSession } from "next-auth/react";
import { faPenToSquare } from "@fortawesome/free-regular-svg-icons"; import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
import SubmitButton from "@/components/SubmitButton"; import SubmitButton from "@/components/SubmitButton";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import TextInput from "@/components/TextInput";
type Props = { type Props = {
togglePasswordFormModal: Function; togglePasswordFormModal: Function;
@ -80,24 +81,25 @@ export default function ChangePassword({
return ( return (
<div className="mx-auto sm:w-[35rem] w-80"> <div className="mx-auto sm:w-[35rem] w-80">
<div className="max-w-[25rem] w-full mx-auto flex flex-col gap-3 justify-between"> <div className="max-w-[25rem] w-full mx-auto flex flex-col gap-2 justify-between">
<p className="text-sm text-sky-700">New Password</p> <p className="text-sm text-black dark:text-white">New Password</p>
<input <TextInput
value={newPassword} value={newPassword}
onChange={(e) => setNewPassword1(e.target.value)} onChange={(e) => setNewPassword1(e.target.value)}
type="password"
placeholder="••••••••••••••" placeholder="••••••••••••••"
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100" type="password"
/> />
<p className="text-sm text-sky-700">Confirm New Password</p>
<input <p className="text-sm text-black dark:text-white">
Confirm New Password
</p>
<TextInput
value={newPassword2} value={newPassword2}
onChange={(e) => setNewPassword2(e.target.value)} onChange={(e) => setNewPassword2(e.target.value)}
type="password"
placeholder="••••••••••••••" placeholder="••••••••••••••"
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100" type="password"
/> />
<SubmitButton <SubmitButton

View File

@ -119,7 +119,9 @@ export default function PrivacySettings({
return ( return (
<div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80"> <div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80">
<div> <div>
<p className="text-sm text-sky-700 mb-2">Profile Visibility</p> <p className="text-sm text-black dark:text-white mb-2">
Profile Visibility
</p>
<Checkbox <Checkbox
label="Make profile private" label="Make profile private"
@ -128,19 +130,21 @@ export default function PrivacySettings({
onClick={() => setUser({ ...user, isPrivate: !user.isPrivate })} onClick={() => setUser({ ...user, isPrivate: !user.isPrivate })}
/> />
<p className="text-gray-500 text-sm"> <p className="text-gray-500 dark:text-gray-300 text-sm">
This will limit who can find and add you to other Collections. This will limit who can find and add you to other Collections.
</p> </p>
{user.isPrivate && ( {user.isPrivate && (
<div> <div>
<p className="text-sm text-sky-700 my-2">Whitelisted Users</p> <p className="text-sm text-black dark:text-white mt-2">
<p className="text-gray-500 text-sm mb-3"> Whitelisted Users
</p>
<p className="text-gray-500 dark:text-gray-300 text-sm mb-3">
Please provide the Username of the users you wish to grant Please provide the Username of the users you wish to grant
visibility to your profile. Separated by comma. visibility to your profile. Separated by comma.
</p> </p>
<textarea <textarea
className="w-full resize-none border rounded-md duration-100 bg-white p-2 outline-none border-sky-100 focus:border-sky-700" className="w-full resize-none border rounded-md duration-100 bg-gray-50 dark:bg-neutral-950 p-2 outline-none border-sky-100 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600"
placeholder="Your profile is hidden from everyone right now..." placeholder="Your profile is hidden from everyone right now..."
value={whitelistedUsersTextbox} value={whitelistedUsersTextbox}
onChange={(e) => { onChange={(e) => {
@ -152,7 +156,9 @@ export default function PrivacySettings({
</div> </div>
<div className="mt-5"> <div className="mt-5">
<p className="text-sm text-sky-700 mb-2">Import/Export Data</p> <p className="text-sm text-black dark:text-white mb-2">
Import/Export Data
</p>
<div className="flex gap-2"> <div className="flex gap-2">
<div <div
@ -162,7 +168,7 @@ export default function PrivacySettings({
> >
<div <div
id="import-dropdown" id="import-dropdown"
className="border border-slate-200 rounded-md bg-white px-2 text-center select-none cursor-pointer text-sky-900 duration-100 hover:border-sky-700" className="border border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600"
> >
Import From Import From
</div> </div>
@ -172,13 +178,13 @@ export default function PrivacySettings({
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
if (target.id !== "import-dropdown") setImportDropdown(false); if (target.id !== "import-dropdown") setImportDropdown(false);
}} }}
className={`absolute top-7 left-0 w-36 py-1 shadow-md border border-sky-100 bg-gray-50 rounded-md flex flex-col z-20`} className={`absolute top-7 left-0 w-36 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
> >
<div className="cursor-pointer rounded-md"> <div className="cursor-pointer rounded-md">
<label <label
htmlFor="import-file" htmlFor="import-file"
title="JSON" title="JSON"
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 duration-100 cursor-pointer" className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
> >
Linkwarden Linkwarden
<input <input
@ -196,7 +202,7 @@ export default function PrivacySettings({
</div> </div>
<Link className="w-fit" href="/api/data"> <Link className="w-fit" href="/api/data">
<div className="border border-slate-200 rounded-md bg-white px-2 text-center select-none cursor-pointer text-sky-900 duration-100 hover:border-sky-700"> <div className="border border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600">
Export Data Export Data
</div> </div>
</Link> </Link>

View File

@ -9,6 +9,7 @@ import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
import SubmitButton from "../../SubmitButton"; import SubmitButton from "../../SubmitButton";
import ProfilePhoto from "../../ProfilePhoto"; import ProfilePhoto from "../../ProfilePhoto";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import TextInput from "@/components/TextInput";
type Props = { type Props = {
toggleSettingsModal: Function; toggleSettingsModal: Function;
@ -102,7 +103,9 @@ export default function ProfileSettings({
<div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80"> <div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80">
<div className="grid sm:grid-cols-2 gap-3 auto-rows-auto"> <div className="grid sm:grid-cols-2 gap-3 auto-rows-auto">
<div className="sm:row-span-2 sm:justify-self-center mx-auto mb-3"> <div className="sm:row-span-2 sm:justify-self-center mx-auto mb-3">
<p className="text-sm text-sky-700 mb-2 text-center">Profile Photo</p> <p className="text-sm text-black dark:text-white mb-2 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 items-center justify-center rounded-full relative">
<ProfilePhoto <ProfilePhoto
src={user.profilePic} src={user.profilePic}
@ -127,7 +130,7 @@ export default function ProfileSettings({
<label <label
htmlFor="upload-photo" htmlFor="upload-photo"
title="PNG or JPG (Max: 3MB)" title="PNG or JPG (Max: 3MB)"
className="border border-slate-200 rounded-md bg-white px-2 text-center select-none cursor-pointer text-sky-900 duration-100 hover:border-sky-700" className="border border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600"
> >
Browse... Browse...
<input <input
@ -145,33 +148,29 @@ export default function ProfileSettings({
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div> <div>
<p className="text-sm text-sky-700 mb-2">Display Name</p> <p className="text-sm text-black dark:text-white mb-2">
<input Display Name
type="text" </p>
value={user.name} <TextInput
value={user.name || ""}
onChange={(e) => setUser({ ...user, name: e.target.value })} onChange={(e) => setUser({ ...user, name: e.target.value })}
className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
<div> <div>
<p className="text-sm text-sky-700 mb-2">Username</p> <p className="text-sm text-black dark:text-white mb-2">Username</p>
<input <TextInput
type="text"
value={user.username || ""} value={user.username || ""}
onChange={(e) => setUser({ ...user, username: e.target.value })} onChange={(e) => setUser({ ...user, username: e.target.value })}
className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
{emailEnabled ? ( {emailEnabled ? (
<div> <div>
<p className="text-sm text-sky-700 mb-2">Email</p> <p className="text-sm text-black dark:text-white mb-2">Email</p>
<input <TextInput
type="text"
value={user.email || ""} value={user.email || ""}
onChange={(e) => setUser({ ...user, email: e.target.value })} onChange={(e) => setUser({ ...user, email: e.target.value })}
className="w-full rounded-md p-2 border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
) : undefined} ) : undefined}
@ -184,15 +183,6 @@ export default function ProfileSettings({
</div> </div>
</div> </div>
{/* <hr /> TODO: Export functionality
<p className="text-sky-700">Data Settings</p>
<div className="w-fit">
<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-700">
Export Data
</div>
</div> */}
<SubmitButton <SubmitButton
onClick={submit} onClick={submit}
loading={submitLoader} loading={submitLoader}

View File

@ -27,12 +27,12 @@ export default function UserModal({
return ( return (
<div className={className}> <div className={className}>
<Tab.Group defaultIndex={defaultIndex}> <Tab.Group defaultIndex={defaultIndex}>
<Tab.List className="flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-sky-700"> <Tab.List className="flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-black dark:text-white">
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" ? "px-2 py-1 bg-sky-200 dark:bg-sky-800 dark:text-white duration-100 rounded-md outline-none"
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" : "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 rounded-md duration-100 outline-none"
} }
> >
Profile Settings Profile Settings
@ -41,8 +41,8 @@ export default function UserModal({
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" ? "px-2 py-1 bg-sky-200 dark:bg-sky-800 dark:text-white duration-100 rounded-md outline-none"
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" : "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 rounded-md duration-100 outline-none"
} }
> >
Privacy Settings Privacy Settings
@ -51,8 +51,8 @@ export default function UserModal({
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" ? "px-2 py-1 bg-sky-200 dark:bg-sky-800 dark:text-white duration-100 rounded-md outline-none"
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" : "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 rounded-md duration-100 outline-none"
} }
> >
Password Password
@ -62,8 +62,8 @@ export default function UserModal({
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
selected selected
? "px-2 py-1 bg-sky-200 duration-100 rounded-md outline-none" ? "px-2 py-1 bg-sky-200 dark:bg-sky-800 duration-100 rounded-md outline-none"
: "px-2 py-1 hover:bg-slate-200 rounded-md duration-100 outline-none" : "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 rounded-md duration-100 outline-none"
} }
> >
Billing Portal Billing Portal

View File

@ -16,14 +16,14 @@ export default function Modal({ toggleModal, className, children }: Props) {
onClickOutside={toggleModal} onClickOutside={toggleModal}
className={`m-auto ${className}`} className={`m-auto ${className}`}
> >
<div className="slide-up relative border-sky-100 rounded-2xl border-solid border shadow-lg p-5 bg-white"> <div className="slide-up relative border-sky-100 dark:border-neutral-700 rounded-2xl border-solid border shadow-lg p-5 bg-white dark:bg-neutral-900">
<div <div
onClick={toggleModal as MouseEventHandler<HTMLDivElement>} onClick={toggleModal as MouseEventHandler<HTMLDivElement>}
className="absolute top-5 left-5 inline-flex rounded-md cursor-pointer hover:bg-slate-200 duration-100 z-20 p-2" className="absolute top-5 left-5 inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 z-20 p-2"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faChevronLeft} icon={faChevronLeft}
className="w-4 h-4 text-gray-500" className="w-4 h-4 text-gray-500 dark:text-gray-300"
/> />
</div> </div>
{children} {children}

View File

@ -10,6 +10,7 @@ import Search from "@/components/Search";
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";
export default function Navbar() { export default function Navbar() {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
@ -22,6 +23,16 @@ export default function Navbar() {
const router = useRouter(); const router = useRouter();
const { theme, setTheme } = useTheme();
const handleToggle = () => {
if (theme === "dark") {
setTheme("light");
} else {
setTheme("dark");
}
};
window.addEventListener("resize", () => setSidebar(false)); window.addEventListener("resize", () => setSidebar(false));
useEffect(() => { useEffect(() => {
@ -33,10 +44,10 @@ export default function Navbar() {
}; };
return ( return (
<div className="flex justify-between gap-2 items-center px-5 py-2 border-solid border-b-sky-100 border-b h-16"> <div className="flex justify-between gap-2 items-center px-5 py-2 border-solid border-b-sky-100 dark:border-b-neutral-700 border-b h-16">
<div <div
onClick={toggleSidebar} onClick={toggleSidebar}
className="inline-flex lg:hidden gap-1 items-center select-none cursor-pointer p-[0.687rem] text-sky-700 rounded-md duration-100 hover:bg-slate-200" className="inline-flex lg:hidden gap-1 items-center select-none cursor-pointer p-[0.687rem] text-gray-500 dark:text-gray-300 rounded-md duration-100 hover:bg-slate-200 dark:hover:bg-neutral-700"
> >
<FontAwesomeIcon icon={faBars} className="w-5 h-5" /> <FontAwesomeIcon icon={faBars} className="w-5 h-5" />
</div> </div>
@ -50,7 +61,7 @@ export default function Navbar() {
method: "CREATE", method: "CREATE",
}); });
}} }}
className="inline-flex gap-1 relative sm:w-[7.2rem] items-center font-semibold select-none cursor-pointer p-[0.687rem] sm:p-2 sm:px-3 rounded-md sm:rounded-full hover:bg-sky-100 text-sky-700 sm:text-white sm:bg-sky-700 sm:hover:bg-sky-600 duration-100 group" className="inline-flex gap-1 relative sm:w-[7.2rem] items-center font-semibold select-none cursor-pointer p-[0.687rem] sm:p-2 sm:px-3 rounded-md sm:rounded-full hover:bg-sky-100 dark:hover:bg-sky-800 sm:dark:hover:bg-sky-600 text-sky-500 sm:text-white sm:bg-sky-700 sm:hover:bg-sky-600 duration-100 group"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faPlus} icon={faPlus}
@ -60,10 +71,9 @@ export default function Navbar() {
New Link New Link
</span> </span>
</div> </div>
<div className="relative"> <div className="relative">
<div <div
className="flex gap-1 group sm:hover:bg-slate-200 sm:hover:p-1 sm:hover:pr-2 duration-100 h-10 rounded-full items-center w-fit bg-white 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"
onClick={() => setProfileDropdown(!profileDropdown)} onClick={() => setProfileDropdown(!profileDropdown)}
id="profile-dropdown" id="profile-dropdown"
> >
@ -73,7 +83,7 @@ export default function Navbar() {
/> />
<p <p
id="profile-dropdown" id="profile-dropdown"
className="font-bold text-sky-700 leading-3 hidden sm:block select-none truncate max-w-[8rem] py-1" className="font-bold text-black dark:text-white leading-3 hidden sm:block select-none truncate max-w-[8rem] py-1"
> >
{account.name} {account.name}
</p> </p>
@ -92,6 +102,13 @@ export default function Navbar() {
setProfileDropdown(!profileDropdown); setProfileDropdown(!profileDropdown);
}, },
}, },
{
name: `Switch to ${theme === "light" ? "Dark" : "Light"}`,
onClick: () => {
handleToggle();
setProfileDropdown(!profileDropdown);
},
},
{ {
name: "Logout", name: "Logout",
onClick: () => { onClick: () => {

View File

@ -3,17 +3,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React from "react"; import React from "react";
import useModalStore from "@/store/modals"; import useModalStore from "@/store/modals";
export default function NoLinksFound() { type Props = {
text?: string;
};
export default function NoLinksFound({ text }: Props) {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
return ( return (
<div className="border border-solid border-sky-100 w-full p-10 rounded-2xl"> <div className="border border-solid border-sky-100 dark:border-neutral-700 w-full p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800">
<p className="text-center text-3xl text-sky-700"> <p className="text-center text-2xl text-black dark:text-white">
You haven&apos;t created any Links Here {text || "You haven't created any Links Here"}
</p> </p>
<br /> <div className="text-center text-black dark:text-white w-full mt-4">
<div className="text-center text-sky-900 text-sm flex items-baseline justify-center gap-1 w-full">
<p>Start by creating a</p>{" "}
<div <div
onClick={() => { onClick={() => {
setModal({ setModal({
@ -22,14 +24,14 @@ export default function NoLinksFound() {
method: "CREATE", method: "CREATE",
}); });
}} }}
className="inline-flex gap-1 relative w-[7.2rem] items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-full text-white bg-sky-700 hover:bg-sky-600 duration-100 group" className="inline-flex gap-1 relative w-[11.4rem] items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-full dark:hover:bg-sky-600 text-white bg-sky-700 hover:bg-sky-600 duration-100 group"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faPlus} icon={faPlus}
className="w-5 h-5 group-hover:ml-9 absolute duration-100" className="w-5 h-5 group-hover:ml-[4.325rem] absolute duration-100"
/> />
<span className="block group-hover:opacity-0 text-right w-full duration-100"> <span className="group-hover:opacity-0 text-right w-full duration-100">
New Link Create New Link
</span> </span>
</div> </div>
</div> </div>

View File

@ -33,7 +33,7 @@ export default function ProfilePhoto({
return error || !src ? ( return error || !src ? (
<div <div
className={`bg-sky-500 text-white h-10 w-10 aspect-square shadow rounded-full border border-slate-200 flex items-center justify-center ${className}`} className={`bg-sky-600 dark:bg-sky-600 text-white h-10 w-10 aspect-square shadow rounded-full border border-slate-200 dark:border-neutral-700 flex items-center justify-center ${className}`}
> >
<FontAwesomeIcon icon={faUser} className="w-1/2 h-1/2 aspect-square" /> <FontAwesomeIcon icon={faUser} className="w-1/2 h-1/2 aspect-square" />
</div> </div>
@ -43,7 +43,7 @@ export default function ProfilePhoto({
src={src} src={src}
height={112} height={112}
width={112} width={112}
className={`h-10 w-10 shadow rounded-full aspect-square border border-slate-200 ${className}`} className={`h-10 w-10 shadow rounded-full aspect-square border border-slate-200 dark:border-neutral-700 ${className}`}
/> />
); );
} }

View File

@ -58,8 +58,10 @@ export default function LinkCard({ link, count }: Props) {
<div className="flex justify-between items-center gap-5 w-full h-full z-0"> <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 flex-col justify-between">
<div className="flex items-baseline gap-1"> <div className="flex items-baseline gap-1">
<p className="text-sm text-sky-500 font-bold">{count + 1}.</p> <p className="text-xs text-gray-500">{count + 1}</p>
<p className="text-lg text-sky-700 font-bold">{link.name}</p> <p className="text-lg text-black">
{link.name || link.description}
</p>
</div> </div>
<p className="text-gray-500 text-sm font-medium"> <p className="text-gray-500 text-sm font-medium">
@ -70,7 +72,7 @@ export default function LinkCard({ link, count }: Props) {
{link.tags.map((e, i) => ( {link.tags.map((e, i) => (
<p <p
key={i} key={i}
className="px-2 py-1 bg-sky-200 text-sky-700 text-xs rounded-3xl cursor-pointer truncate max-w-[10rem]" className="px-2 py-1 bg-sky-200 text-black text-xs rounded-3xl cursor-pointer truncate max-w-[10rem]"
> >
{e.name} {e.name}
</p> </p>
@ -79,7 +81,7 @@ export default function LinkCard({ link, count }: Props) {
</div> </div>
<div className="flex gap-2 items-center flex-wrap mt-2"> <div className="flex gap-2 items-center flex-wrap mt-2">
<p className="text-gray-500">{formattedDate}</p> <p className="text-gray-500">{formattedDate}</p>
<div className="text-sky-500 font-bold flex items-center gap-1"> <div className="text-black flex items-center gap-1">
<p>{url ? url.host : link.url}</p> <p>{url ? url.host : link.url}</p>
</div> </div>
</div> </div>

View File

@ -20,13 +20,15 @@ export default function RadioButton({ label, state, onClick }: Props) {
/> />
<FontAwesomeIcon <FontAwesomeIcon
icon={faCircleCheck} icon={faCircleCheck}
className="w-5 h-5 text-sky-700 peer-checked:block hidden" className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:block hidden"
/> />
<FontAwesomeIcon <FontAwesomeIcon
icon={faCircle} icon={faCircle}
className="w-5 h-5 text-sky-700 peer-checked:hidden block" className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:hidden block"
/> />
<span className="text-sky-900 rounded select-none">{label}</span> <span className="text-black dark:text-white rounded select-none">
{label}
</span>
</label> </label>
); );
} }

View File

@ -1,6 +1,9 @@
export default function RequiredBadge() { export default function RequiredBadge() {
return ( return (
<span title="Required Field" className="text-sky-700 cursor-help"> <span
title="Required Field"
className="text-black dark:text-white cursor-help"
>
{" "} {" "}
* *
</span> </span>

View File

@ -24,7 +24,7 @@ export default function Search() {
> >
<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 group-hover:text-sky-700" className="inline-flex w-fit absolute left-2 pointer-events-none rounded-md p-1 text-sky-500 dark:text-sky-500"
> >
<FontAwesomeIcon icon={faMagnifyingGlass} className="w-5 h-5" /> <FontAwesomeIcon icon={faMagnifyingGlass} className="w-5 h-5" />
</label> </label>
@ -44,7 +44,7 @@ export default function Search() {
router.push("/search/" + encodeURIComponent(searchQuery)) router.push("/search/" + encodeURIComponent(searchQuery))
} }
autoFocus={searchBox} autoFocus={searchBox}
className="border border-sky-100 rounded-md pl-10 py-2 pr-2 w-44 sm:w-60 focus:border-sky-700 md:focus:w-80 hover:border-sky-700 duration-100 outline-none" 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

@ -51,22 +51,23 @@ export default function Sidebar({ className }: { className?: string }) {
return ( return (
<div <div
className={`bg-gray-100 h-full w-64 xl:w-80 overflow-y-auto border-solid border-r-sky-100 px-2 border z-20 ${className}`} className={`bg-gray-100 dark:bg-neutral-800 h-full w-64 xl:w-80 overflow-y-auto border-solid border dark:border-neutral-800 border-r-sky-100 dark:border-r-neutral-700 px-2 z-20 ${className}`}
> >
<div className="flex justify-center gap-2 mt-2"> <div className="flex justify-center gap-2 mt-2">
<Link <Link
href="/dashboard" href="/dashboard"
className={`${ className={`${
active === "/dashboard" active === "/dashboard"
? "bg-sky-200" ? "bg-sky-200 dark:bg-sky-800"
: "hover:bg-slate-200 bg-gray-100" : "hover:bg-slate-200 hover:dark:bg-neutral-700"
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex justify-center flex-col items-center gap-1 w-full`} } outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex justify-center flex-col items-center gap-1 w-full`}
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faChartSimple} icon={faChartSimple}
className={`w-8 h-8 drop-shadow text-sky-500`} className={`w-8 h-8 drop-shadow text-sky-500 dark:text-sky-500`}
/> />
<p className="text-sky-700 text-xs xl:text-sm font-semibold">
<p className="text-black dark:text-white text-xs xl:text-sm font-semibold">
Dashboard Dashboard
</p> </p>
</Link> </Link>
@ -75,28 +76,34 @@ export default function Sidebar({ className }: { className?: string }) {
href="/links" href="/links"
className={`${ className={`${
active === "/links" active === "/links"
? "bg-sky-200" ? "bg-sky-200 dark:bg-sky-800"
: "hover:bg-slate-200 bg-gray-100" : "hover:bg-slate-200 hover:dark:bg-neutral-700"
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex justify-center flex-col items-center gap-1 w-full`} } outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex justify-center flex-col items-center gap-1 w-full`}
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faLink} icon={faLink}
className={`w-8 h-8 drop-shadow text-sky-500`} className={`w-8 h-8 drop-shadow text-sky-500 dark:text-sky-500`}
/> />
<p className="text-sky-700 text-xs xl:text-sm font-semibold">Links</p>
<p className="text-black dark:text-white text-xs xl:text-sm font-semibold">
Links
</p>
</Link> </Link>
<Link <Link
href="/collections" href="/collections"
className={`${ className={`${
active === "/collections" ? "bg-sky-200" : "hover:bg-slate-200" active === "/collections"
? "bg-sky-200 dark:bg-sky-800"
: "hover:bg-slate-200 hover:dark:bg-neutral-700"
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex justify-center flex-col items-center gap-1 w-full`} } outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex justify-center flex-col items-center gap-1 w-full`}
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faFolder} icon={faFolder}
className={`w-8 h-8 drop-shadow text-sky-500`} className={`w-8 h-8 drop-shadow text-sky-500 dark:text-sky-500`}
/> />
<p className="text-sky-700 text-xs xl:text-sm font-semibold">
<p className="text-black dark:text-white text-xs xl:text-sm font-semibold">
Collections Collections
</p> </p>
</Link> </Link>
@ -107,7 +114,7 @@ export default function Sidebar({ className }: { className?: string }) {
onClick={() => { onClick={() => {
setCollectionDisclosure(!collectionDisclosure); setCollectionDisclosure(!collectionDisclosure);
}} }}
className="flex items-center justify-between text-sm w-full text-left mb-2 pl-2 font-bold text-gray-500 mt-5" className="flex items-center justify-between text-sm w-full text-left mb-2 pl-2 font-bold text-gray-500 dark:text-gray-300 mt-5"
> >
<p>Collections</p> <p>Collections</p>
@ -136,8 +143,8 @@ export default function Sidebar({ className }: { className?: string }) {
<div <div
className={`${ className={`${
active === `/collections/${e.id}` active === `/collections/${e.id}`
? "bg-sky-200" ? "bg-sky-200 dark:bg-sky-800"
: "hover:bg-slate-200 bg-gray-100" : "hover:bg-slate-200 hover:dark:bg-neutral-700"
} duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`} } duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
> >
<FontAwesomeIcon <FontAwesomeIcon
@ -146,7 +153,7 @@ export default function Sidebar({ className }: { className?: string }) {
style={{ color: e.color }} style={{ color: e.color }}
/> />
<p className="text-sky-700 truncate w-full pr-7"> <p className="text-black dark:text-white truncate w-full pr-7">
{e.name} {e.name}
</p> </p>
</div> </div>
@ -157,7 +164,7 @@ export default function Sidebar({ className }: { className?: string }) {
<div <div
className={`duration-100 py-1 px-2 flex items-center gap-2 w-full rounded-md h-8 capitalize`} className={`duration-100 py-1 px-2 flex items-center gap-2 w-full rounded-md h-8 capitalize`}
> >
<p className="text-gray-500 text-xs font-semibold truncate w-full pr-7"> <p className="text-gray-500 dark:text-gray-300 text-xs font-semibold truncate w-full pr-7">
You Have No Collections... You Have No Collections...
</p> </p>
</div> </div>
@ -170,7 +177,7 @@ export default function Sidebar({ className }: { className?: string }) {
onClick={() => { onClick={() => {
setTagDisclosure(!tagDisclosure); setTagDisclosure(!tagDisclosure);
}} }}
className="flex items-center justify-between text-sm w-full text-left mb-2 pl-2 font-bold text-gray-500 mt-5" className="flex items-center justify-between text-sm w-full text-left mb-2 pl-2 font-bold text-gray-500 dark:text-gray-300 mt-5"
> >
<p>Tags</p> <p>Tags</p>
<FontAwesomeIcon <FontAwesomeIcon
@ -196,16 +203,16 @@ export default function Sidebar({ className }: { className?: string }) {
<div <div
className={`${ className={`${
active === `/tags/${e.id}` active === `/tags/${e.id}`
? "bg-sky-200" ? "bg-sky-200 dark:bg-sky-800"
: "hover:bg-slate-200 bg-gray-100" : "hover:bg-slate-200 hover:dark:bg-neutral-700"
} duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`} } duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faHashtag} icon={faHashtag}
className="w-4 h-4 text-sky-500 mt-1" className="w-4 h-4 text-sky-500 dark:text-sky-500 mt-1"
/> />
<p className="text-sky-700 truncate w-full pr-7"> <p className="text-black dark:text-white truncate w-full pr-7">
{e.name} {e.name}
</p> </p>
</div> </div>
@ -216,7 +223,7 @@ export default function Sidebar({ className }: { className?: string }) {
<div <div
className={`duration-100 py-1 px-2 flex items-center gap-2 w-full rounded-md h-8 capitalize`} className={`duration-100 py-1 px-2 flex items-center gap-2 w-full rounded-md h-8 capitalize`}
> >
<p className="text-gray-500 text-xs font-semibold truncate w-full pr-7"> <p className="text-gray-500 dark:text-gray-300 text-xs font-semibold truncate w-full pr-7">
You Have No Tags... You Have No Tags...
</p> </p>
</div> </div>

View File

@ -21,9 +21,11 @@ export default function SortDropdown({
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
if (target.id !== "sort-dropdown") toggleSortDropdown(); if (target.id !== "sort-dropdown") toggleSortDropdown();
}} }}
className="absolute top-8 right-0 border border-sky-100 shadow-md bg-gray-50 rounded-md p-2 z-20 w-48" className="absolute top-8 right-0 border border-sky-100 dark:border-neutral-700 shadow-md bg-gray-50 dark:bg-neutral-800 rounded-md p-2 z-20 w-48"
> >
<p className="mb-2 text-sky-900 text-center font-semibold">Sort by</p> <p className="mb-2 text-black dark:text-white text-center font-semibold">
Sort by
</p>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<RadioButton <RadioButton
label="Date (Newest First)" label="Date (Newest First)"

33
components/TextInput.tsx Normal file
View File

@ -0,0 +1,33 @@
import { ChangeEventHandler, KeyboardEventHandler } from "react";
type Props = {
autoFocus?: boolean;
value?: string;
type?: string;
placeholder?: string;
onChange: ChangeEventHandler<HTMLInputElement>;
onKeyDown?: KeyboardEventHandler<HTMLInputElement> | undefined;
className?: string;
};
export default function TextInput({
autoFocus,
value,
type,
placeholder,
onChange,
onKeyDown,
className,
}: Props) {
return (
<input
autoFocus={autoFocus}
type={type ? type : "text"}
placeholder={placeholder}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
className={`w-full rounded-md p-2 border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-950 border-solid border outline-none focus:border-sky-300 focus:dark:border-sky-600 duration-100 ${className}`}
/>
);
}

View File

@ -0,0 +1,29 @@
import { useTheme } from "next-themes";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
export default function ToggleDarkMode() {
const { theme, setTheme } = useTheme();
const handleToggle = () => {
if (theme === "dark") {
setTheme("light");
} else {
setTheme("dark");
}
};
return (
<div
className="flex gap-1 duration-100 h-10 rounded-full items-center w-fit cursor-pointer"
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
icon={theme === "dark" ? faSun : faMoon}
className="w-1/2 h-1/2"
/>
</div>
</div>
);
}

View File

@ -12,7 +12,7 @@ export default function useLinks(
pinnedOnly, pinnedOnly,
collectionId, collectionId,
tagId, tagId,
}: Omit<LinkRequestQuery, "cursor"> = { sort: 0 } }: LinkRequestQuery = { sort: 0 }
) { ) {
const { links, setLinks, resetLinks } = useLinkStore(); const { links, setLinks, resetLinks } = useLinkStore();
const router = useRouter(); const router = useRouter();
@ -33,7 +33,7 @@ export default function useLinks(
const encodedData = encodeURIComponent(JSON.stringify(requestBody)); const encodedData = encodeURIComponent(JSON.stringify(requestBody));
const response = await fetch( const response = await fetch(
`/api/routes/links?body=${encodeURIComponent(encodedData)}` `/api/links?body=${encodeURIComponent(encodedData)}`
); );
const data = await response.json(); const data = await response.json();

View File

@ -1,4 +1,6 @@
import { useTheme } from "next-themes";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
interface Props { interface Props {
@ -7,9 +9,11 @@ interface Props {
} }
export default function CenteredForm({ text, children }: Props) { export default function CenteredForm({ text, children }: Props) {
const { theme } = useTheme();
return ( return (
<div className="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center p-2"> <div className="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center p-5">
<div className="m-auto flex flex-col gap-2"> <div className="m-auto flex flex-col gap-2">
{theme === "light" ? (
<Image <Image
src="/linkwarden.png" src="/linkwarden.png"
width={518} width={518}
@ -17,14 +21,27 @@ export default function CenteredForm({ text, children }: Props) {
alt="Linkwarden" alt="Linkwarden"
className="h-12 w-fit mx-auto" className="h-12 w-fit mx-auto"
/> />
) : (
<Image
src="/linkwarden_darkmode.png"
width={518}
height={145}
alt="Linkwarden"
className="h-12 w-fit mx-auto"
/>
)}
{text ? ( {text ? (
<p className="text-lg sm:w-[30rem] w-80 mx-auto font-semibold text-black px-2 text-center"> <p className="text-lg sm:w-[30rem] w-80 mx-auto font-semibold text-black dark:text-white px-2 text-center">
{text} {text}
</p> </p>
) : undefined} ) : undefined}
{children} {children}
<p className="text-center text-xs text-gray-500"> <p className="text-center text-xs text-gray-500 mb-5 dark:text-gray-400">
© {new Date().getFullYear()} Linkwarden. All rights reserved. © {new Date().getFullYear()}{" "}
<Link href="https://linkwarden.app" className="font-semibold">
Linkwarden
</Link>
. All rights reserved.
</p> </p>
</div> </div>
</div> </div>

View File

@ -36,7 +36,7 @@ export default function MainLayout({ children }: Props) {
<Sidebar className="fixed top-0" /> <Sidebar className="fixed top-0" />
</div> </div>
<div className="w-full lg:ml-64 xl:ml-80"> <div className="w-full flex flex-col h-screen lg:ml-64 xl:ml-80">
<Navbar /> <Navbar />
{children} {children}
</div> </div>

View File

@ -2,8 +2,7 @@ import Stripe from "stripe";
export default async function checkSubscription( export default async function checkSubscription(
stripeSecretKey: string, stripeSecretKey: string,
email: string, email: string
priceId: string
) { ) {
const stripe = new Stripe(stripeSecretKey, { const stripe = new Stripe(stripeSecretKey, {
apiVersion: "2022-11-15", apiVersion: "2022-11-15",
@ -33,11 +32,7 @@ export default async function checkSubscription(
new Date((subscription.canceled_at + secondsInTwoWeeks) * 1000) new Date((subscription.canceled_at + secondsInTwoWeeks) * 1000)
); );
return ( return subscription?.items?.data[0].plan && isNotCanceledOrHasTime;
subscription?.items?.data?.some(
(subscriptionItem) => subscriptionItem?.plan?.id === priceId
) && isNotCanceledOrHasTime
);
} }
); );

View File

@ -26,11 +26,11 @@ export default async function getLink(userId: number, body: string) {
}; };
else if (query.sort === Sort.DescriptionAZ) else if (query.sort === Sort.DescriptionAZ)
order = { order = {
name: "asc", description: "asc",
}; };
else if (query.sort === Sort.DescriptionZA) else if (query.sort === Sort.DescriptionZA)
order = { order = {
name: "desc", description: "desc",
}; };
const links = await prisma.link.findMany({ const links = await prisma.link.findMany({

View File

@ -21,7 +21,7 @@ export default async function postLink(
} }
if (!link.collection.name) { if (!link.collection.name) {
link.collection.name = "Unnamed Collection"; link.collection.name = "Unorganized";
} }
link.collection.name = link.collection.name.trim(); link.collection.name = link.collection.name.trim();

View File

@ -32,11 +32,15 @@ export default async function updateLink(
const isCollectionOwner = const isCollectionOwner =
targetLink?.collection.ownerId === link.collection.ownerId && targetLink?.collection.ownerId === link.collection.ownerId &&
link.collection.ownerId === userId && link.collection.ownerId === userId;
targetLink?.collection.ownerId === userId;
const unauthorizedSwitchCollection =
!isCollectionOwner && targetLink?.collection.id !== link.collection.id;
console.log(isCollectionOwner);
// Makes sure collection members (non-owners) cannot move a link to/from a collection. // Makes sure collection members (non-owners) cannot move a link to/from a collection.
if (!isCollectionOwner) if (unauthorizedSwitchCollection)
return { return {
response: "You can't move a link to/from a collection you don't own.", response: "You can't move a link to/from a collection you don't own.",
status: 401, status: 401,
@ -54,15 +58,11 @@ export default async function updateLink(
data: { data: {
name: link.name, name: link.name,
description: link.description, description: link.description,
collection: collection: {
targetLink?.collection.ownerId === link.collection.ownerId &&
link.collection.ownerId === userId
? {
connect: { connect: {
id: link.collection.id, id: link.collection.id,
}, },
} },
: undefined,
tags: { tags: {
set: [], set: [],
connectOrCreate: link.tags.map((tag) => ({ connectOrCreate: link.tags.map((tag) => ({
@ -99,14 +99,14 @@ export default async function updateLink(
}, },
}); });
if (targetLink.collection.id !== link.collection.id) { if (targetLink?.collection.id !== link.collection.id) {
await moveFile( await moveFile(
`archives/${targetLink.collection.id}/${link.id}.pdf`, `archives/${targetLink?.collection.id}/${link.id}.pdf`,
`archives/${link.collection.id}/${link.id}.pdf` `archives/${link.collection.id}/${link.id}.pdf`
); );
await moveFile( await moveFile(
`archives/${targetLink.collection.id}/${link.id}.png`, `archives/${targetLink?.collection.id}/${link.id}.png`,
`archives/${link.collection.id}/${link.id}.png` `archives/${link.collection.id}/${link.id}.png`
); );
} }

View File

@ -160,12 +160,10 @@ export default async function updateUser(
} }
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const PRICE_ID = process.env.PRICE_ID;
if (STRIPE_SECRET_KEY && PRICE_ID && emailEnabled) if (STRIPE_SECRET_KEY && emailEnabled)
await updateCustomerEmail( await updateCustomerEmail(
STRIPE_SECRET_KEY, STRIPE_SECRET_KEY,
PRICE_ID,
sessionUser.email, sessionUser.email,
user.email as string user.email as string
); );

View File

@ -1,5 +1,4 @@
import Stripe from "stripe"; import Stripe from "stripe";
import checkSubscription from "./checkSubscription";
export default async function paymentCheckout( export default async function paymentCheckout(
stripeSecretKey: string, stripeSecretKey: string,

View File

@ -125,7 +125,7 @@ const fileNotFoundTemplate = `<!DOCTYPE html>
<h2>It is possible that the file you're looking for either doesn't exist or hasn't been created yet.</h2> <h2>It is possible that the file you're looking for either doesn't exist or hasn't been created yet.</h2>
<h3>Some possible reasons are:</h3> <h3>Some possible reasons are:</h3>
<ul> <ul>
<li>You are trying to access a file too early, before it has been fully archived.</li> <li>You are trying to access a file too early, before it has been fully archived. If that's the case, refreshing the page might resolve the issue.</li>
<li>The file doesn't exist either because it encountered an error while being archived, or it simply doesn't exist.</li> <li>The file doesn't exist either because it encountered an error while being archived, or it simply doesn't exist.</li>
</ul> </ul>
</body> </body>

View File

@ -2,7 +2,6 @@ import Stripe from "stripe";
export default async function updateCustomerEmail( export default async function updateCustomerEmail(
stripeSecretKey: string, stripeSecretKey: string,
priceId: string,
email: string, email: string,
newEmail: string newEmail: string
) { ) {
@ -30,11 +29,7 @@ export default async function updateCustomerEmail(
new Date((subscription.canceled_at + secondsInTwoWeeks) * 1000) new Date((subscription.canceled_at + secondsInTwoWeeks) * 1000)
); );
return ( return subscription?.items?.data[0].plan && isNotCanceledOrHasTime;
subscription?.items?.data?.some(
(subscriptionItem) => subscriptionItem?.plan?.id === priceId
) && isNotCanceledOrHasTime
);
} }
); );

View File

@ -17,7 +17,7 @@ const getPublicCollectionData = async (
const encodedData = encodeURIComponent(JSON.stringify(requestBody)); const encodedData = encodeURIComponent(JSON.stringify(requestBody));
const res = await fetch( const res = await fetch(
"/api/public/routes/collections?body=" + encodeURIComponent(encodedData) "/api/public/collections?body=" + encodeURIComponent(encodedData)
); );
const data = await res.json(); const data = await res.json();

View File

@ -8,7 +8,7 @@ export default async function getPublicUserData({
id?: number; id?: number;
}) { }) {
const response = await fetch( const response = await fetch(
`/api/routes/users?id=${id}&${ `/api/users?id=${id}&${
username ? `username=${username?.toLowerCase()}` : undefined username ? `username=${username?.toLowerCase()}` : undefined
}` }`
); );

View File

@ -0,0 +1,4 @@
export default function htmlDecode(input: string) {
var doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}

View File

@ -36,6 +36,7 @@
"eslint-config-next": "13.4.9", "eslint-config-next": "13.4.9",
"next": "13.4.12", "next": "13.4.12",
"next-auth": "^4.22.1", "next-auth": "^4.22.1",
"next-themes": "^0.2.1",
"nodemailer": "^6.9.3", "nodemailer": "^6.9.3",
"playwright": "^1.35.1", "playwright": "^1.35.1",
"react": "18.2.0", "react": "18.2.0",

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect } from "react";
import "@/styles/globals.css"; import "@/styles/globals.css";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
@ -6,6 +6,7 @@ import Head from "next/head";
import AuthRedirect from "@/layouts/AuthRedirect"; import AuthRedirect from "@/layouts/AuthRedirect";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { Session } from "next-auth"; import { Session } from "next-auth";
import { ThemeProvider } from "next-themes";
export default function App({ export default function App({
Component, Component,
@ -13,6 +14,13 @@ export default function App({
}: AppProps<{ }: AppProps<{
session: Session; session: Session;
}>) { }>) {
const defaultTheme: "light" | "dark" = "dark";
useEffect(() => {
if (!localStorage.getItem("theme"))
localStorage.setItem("theme", defaultTheme);
}, []);
return ( return (
<SessionProvider session={pageProps.session}> <SessionProvider session={pageProps.session}>
<Head> <Head>
@ -37,13 +45,18 @@ export default function App({
/> />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
</Head> </Head>
<AuthRedirect>
<ThemeProvider attribute="class">
<Toaster <Toaster
position="top-center" position="top-center"
reverseOrder={false} reverseOrder={false}
toastOptions={{ className: "border border-sky-100" }} toastOptions={{
className:
"border border-sky-100 dark:dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
}}
/> />
<AuthRedirect>
<Component {...pageProps} /> <Component {...pageProps} />
</ThemeProvider>
</AuthRedirect> </AuthRedirect>
</SessionProvider> </SessionProvider>
); );

View File

@ -91,7 +91,6 @@ export const authOptions: AuthOptions = {
// Using the `...rest` parameter to be able to narrow down the type based on `trigger` // Using the `...rest` parameter to be able to narrow down the type based on `trigger`
async jwt({ token, trigger, session, user }) { async jwt({ token, trigger, session, user }) {
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const PRICE_ID = process.env.PRICE_ID;
const NEXT_PUBLIC_TRIAL_PERIOD_DAYS = const NEXT_PUBLIC_TRIAL_PERIOD_DAYS =
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS; process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS;
@ -108,13 +107,11 @@ export const authOptions: AuthOptions = {
if ( if (
STRIPE_SECRET_KEY && STRIPE_SECRET_KEY &&
PRICE_ID &&
(trigger || subscriptionIsTimesUp || !token.isSubscriber) (trigger || subscriptionIsTimesUp || !token.isSubscriber)
) { ) {
const subscription = await checkSubscription( const subscription = await checkSubscription(
STRIPE_SECRET_KEY, STRIPE_SECRET_KEY,
token.email as string, token.email as string
PRICE_ID
); );
if (subscription.subscriptionCanceledAt) { if (subscription.subscriptionCanceledAt) {

View File

@ -2,18 +2,27 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next"; import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/auth/[...nextauth]"; import { authOptions } from "@/pages/api/auth/[...nextauth]";
import paymentCheckout from "@/lib/api/paymentCheckout"; import paymentCheckout from "@/lib/api/paymentCheckout";
import { Plan } from "@/types/global";
export default async function users(req: NextApiRequest, res: NextApiResponse) { export default async function users(req: NextApiRequest, res: NextApiResponse) {
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const PRICE_ID = process.env.PRICE_ID; const MONTHLY_PRICE_ID = process.env.MONTHLY_PRICE_ID;
const YEARLY_PRICE_ID = process.env.YEARLY_PRICE_ID;
const session = await getServerSession(req, res, authOptions); const session = await getServerSession(req, res, authOptions);
if (!session?.user?.id) if (!session?.user?.id)
return res.status(401).json({ response: "You must be logged in." }); return res.status(401).json({ response: "You must be logged in." });
else if (!STRIPE_SECRET_KEY || !PRICE_ID) { else if (!STRIPE_SECRET_KEY || !MONTHLY_PRICE_ID || !YEARLY_PRICE_ID) {
return res.status(400).json({ response: "Payment is disabled." }); return res.status(400).json({ response: "Payment is disabled." });
} }
let PRICE_ID = MONTHLY_PRICE_ID;
if ((Number(req.query.plan) as unknown as Plan) === Plan.monthly)
PRICE_ID = MONTHLY_PRICE_ID;
else if ((Number(req.query.plan) as unknown as Plan) === Plan.yearly)
PRICE_ID = YEARLY_PRICE_ID;
if (req.method === "GET") { if (req.method === "GET") {
const users = await paymentCheckout( const users = await paymentCheckout(
STRIPE_SECRET_KEY, STRIPE_SECRET_KEY,

View File

@ -7,6 +7,7 @@ import { useSession } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import CenteredForm from "@/layouts/CenteredForm"; import CenteredForm from "@/layouts/CenteredForm";
import TextInput from "@/components/TextInput";
export default function Subscribe() { export default function Subscribe() {
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
@ -39,28 +40,30 @@ export default function Subscribe() {
return ( return (
<CenteredForm> <CenteredForm>
<div className="p-2 mx-auto flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 rounded-2xl shadow-md border border-sky-100"> <div className="p-4 mx-auto flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 dark:border-neutral-700 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
<p className="text-2xl text-center text-black font-bold"> <p className="text-2xl text-center text-black dark:text-white font-bold">
Choose a Username (Last step) Choose a Username (Last step)
</p> </p>
<div> <div>
<p className="text-sm text-sky-700 w-fit font-semibold mb-1"> <p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Username Username
</p> </p>
<input <TextInput
type="text"
placeholder="john" placeholder="john"
value={inputedUsername} value={inputedUsername}
className="bg-white"
onChange={(e) => setInputedUsername(e.target.value)} onChange={(e) => setInputedUsername(e.target.value)}
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
<div> <div>
<p className="text-md text-gray-500 mt-1"> <p className="text-md text-gray-500 dark:text-gray-400 mt-1">
Feel free to reach out to us at{" "} Feel free to reach out to us at{" "}
<a className="font-semibold" href="mailto:support@linkwarden.app"> <a
className="font-semibold underline"
href="mailto:support@linkwarden.app"
>
support@linkwarden.app support@linkwarden.app
</a>{" "} </a>{" "}
in case of any issues. in case of any issues.
@ -76,7 +79,7 @@ export default function Subscribe() {
<div <div
onClick={() => signOut()} onClick={() => signOut()}
className="w-fit mx-auto cursor-pointer text-gray-500 font-semibold " className="w-fit mx-auto cursor-pointer text-gray-500 dark:text-gray-400 font-semibold "
> >
Sign Out Sign Out
</div> </div>

View File

@ -50,7 +50,7 @@ export default function Index() {
return ( return (
<MainLayout> <MainLayout>
<div className="p-5 flex flex-col gap-5 w-full"> <div className="p-5 flex flex-col gap-5 w-full">
<div className="bg-gradient-to-tr from-sky-100 from-10% via-gray-100 via-20% rounded-2xl shadow min-h-[10rem] p-5 flex gap-5 flex-col justify-between"> <div className="bg-gradient-to-tr from-sky-100 dark:from-gray-800 from-10% via-gray-100 via-20% to-white dark:to-neutral-800 to-100% rounded-2xl shadow min-h-[10rem] p-5 flex gap-5 flex-col justify-between">
<div className="flex flex-col sm:flex-row gap-3 justify-between items-center sm:items-start"> <div className="flex flex-col sm:flex-row gap-3 justify-between items-center sm:items-start">
{activeCollection && ( {activeCollection && (
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
@ -60,7 +60,7 @@ export default function Index() {
style={{ color: activeCollection?.color }} style={{ color: activeCollection?.color }}
className="sm:w-8 sm:h-8 w-6 h-6 mt-3 drop-shadow" className="sm:w-8 sm:h-8 w-6 h-6 mt-3 drop-shadow"
/> />
<p className="sm:text-4xl text-3xl capitalize text-sky-700 font-bold w-full py-1 break-words hyphens-auto"> <p className="sm:text-4xl text-3xl capitalize text-black dark:text-white w-full py-1 break-words hyphens-auto">
{activeCollection?.name} {activeCollection?.name}
</p> </p>
</div> </div>
@ -107,7 +107,7 @@ export default function Index() {
.slice(0, 4)} .slice(0, 4)}
{activeCollection?.members.length && {activeCollection?.members.length &&
activeCollection.members.length - 4 > 0 ? ( activeCollection.members.length - 4 > 0 ? (
<div className="h-10 w-10 text-white flex items-center justify-center rounded-full border-[3px] bg-sky-700 border-sky-100 -mr-3"> <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">
+{activeCollection?.members?.length - 4} +{activeCollection?.members?.length - 4}
</div> </div>
) : null} ) : null}
@ -116,19 +116,19 @@ export default function Index() {
) : null} ) : null}
</div> </div>
<div className="text-gray-600 flex justify-between items-end gap-5"> <div className="text-black dark:text-white flex justify-between items-end gap-5">
<p>{activeCollection?.description}</p> <p>{activeCollection?.description}</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative"> <div className="relative">
<div <div
onClick={() => setSortDropdown(!sortDropdown)} onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown" id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 duration-100 p-1" className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faSort} icon={faSort}
id="sort-dropdown" id="sort-dropdown"
className="w-5 h-5 text-gray-500" className="w-5 h-5 text-gray-500 dark:text-gray-300"
/> />
</div> </div>
@ -144,31 +144,18 @@ export default function Index() {
<div <div
onClick={() => setExpandDropdown(!expandDropdown)} onClick={() => setExpandDropdown(!expandDropdown)}
id="expand-dropdown" id="expand-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 duration-100 p-1" className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faEllipsis} icon={faEllipsis}
id="expand-dropdown" id="expand-dropdown"
title="More" title="More"
className="w-5 h-5 text-gray-500" className="w-5 h-5 text-gray-500 dark:text-gray-300"
/> />
</div> </div>
{expandDropdown ? ( {expandDropdown ? (
<Dropdown <Dropdown
items={[ items={[
permissions === true || permissions?.canCreate
? {
name: "Add Link Here",
onClick: () => {
setModal({
modal: "LINK",
state: true,
method: "CREATE",
});
setExpandDropdown(false);
},
}
: undefined,
permissions === true permissions === true
? { ? {
name: "Edit Collection Info", name: "Edit Collection Info",

View File

@ -37,9 +37,9 @@ export default function Collections() {
<div className="flex gap-2"> <div className="flex gap-2">
<FontAwesomeIcon <FontAwesomeIcon
icon={faFolder} icon={faFolder}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 drop-shadow" className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
/> />
<p className="sm:text-4xl text-3xl capitalize text-sky-700 font-bold"> <p className="sm:text-4xl text-3xl capitalize text-black dark:text-white">
All Collections All Collections
</p> </p>
</div> </div>
@ -47,12 +47,12 @@ export default function Collections() {
<div <div
onClick={() => setExpandDropdown(!expandDropdown)} onClick={() => setExpandDropdown(!expandDropdown)}
id="expand-dropdown" id="expand-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 duration-100 p-1" className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faEllipsis} icon={faEllipsis}
id="expand-dropdown" id="expand-dropdown"
className="w-5 h-5 text-gray-500" className="w-5 h-5 text-gray-500 dark:text-gray-300"
/> />
</div> </div>
@ -86,12 +86,12 @@ export default function Collections() {
<div <div
onClick={() => setSortDropdown(!sortDropdown)} onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown" id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 duration-100 p-1" className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faSort} icon={faSort}
id="sort-dropdown" id="sort-dropdown"
className="w-5 h-5 text-gray-500" className="w-5 h-5 text-gray-500 dark:text-gray-300"
/> />
</div> </div>
@ -111,7 +111,7 @@ export default function Collections() {
})} })}
<div <div
className="p-5 self-stretch bg-gradient-to-tr from-sky-100 from-10% via-gray-100 via-20% min-h-[12rem] rounded-2xl cursor-pointer shadow duration-100 hover:shadow-none flex flex-col gap-4 justify-center items-center group" className="p-5 self-stretch bg-gradient-to-tr from-sky-100 dark:from-gray-800 from-10% via-gray-100 via-20% to-white dark:to-neutral-800 to-100% min-h-[12rem] rounded-2xl cursor-pointer shadow duration-100 hover:shadow-none flex flex-col gap-4 justify-center items-center group"
onClick={() => { onClick={() => {
setModal({ setModal({
modal: "COLLECTION", modal: "COLLECTION",
@ -120,12 +120,12 @@ export default function Collections() {
}); });
}} }}
> >
<p className="text-sky-900 group-hover:opacity-0 duration-100"> <p className="text-black dark:text-white group-hover:opacity-0 duration-100">
New Collection New Collection
</p> </p>
<FontAwesomeIcon <FontAwesomeIcon
icon={faPlus} icon={faPlus}
className="w-8 h-8 text-sky-500 group-hover:w-12 group-hover:h-12 group-hover:-mt-10 duration-100" className="w-8 h-8 text-sky-500 dark:text-sky-500 group-hover:w-12 group-hover:h-12 group-hover:-mt-10 duration-100"
/> />
</div> </div>
</div> </div>

View File

@ -5,18 +5,18 @@ import React from "react";
export default function EmailConfirmaion() { export default function EmailConfirmaion() {
return ( return (
<CenteredForm> <CenteredForm>
<div className="p-2 sm:w-[30rem] w-80 rounded-2xl shadow-md m-auto border border-sky-100 bg-slate-50 text-sky-800"> <div className="p-4 sm:w-[30rem] w-80 rounded-2xl shadow-md m-auto border border-sky-100 dark:border-neutral-700 bg-slate-50 text-black dark:text-white dark:bg-neutral-800">
<p className="text-center text-xl font-bold mb-2 text-black"> <p className="text-center text-xl font-bold mb-2">
Please check your Email Please check your Email
</p> </p>
<p>A sign in link has been sent to your email address.</p> <p>A sign in link has been sent to your email address.</p>
<p>You can safely close this page.</p> <p>You can safely close this page.</p>
<hr className="my-5" /> <hr className="my-5 dark:border-neutral-700" />
<p className="text-sm text-gray-500 "> <p className="text-sm text-gray-500 dark:text-gray-400">
If you didn&apos;t receive anything, go to the{" "} If you didn&apos;t receive anything, go to the{" "}
<Link href="/forgot" className="font-bold"> <Link href="/forgot" className="font-bold underline">
Password Recovery Password Recovery
</Link>{" "} </Link>{" "}
page and enter your Email to resend the sign in link. page and enter your Email to resend the sign in link.

View File

@ -2,15 +2,13 @@ import useCollectionStore from "@/store/collections";
import { import {
faChartSimple, faChartSimple,
faChevronDown, faChevronDown,
faThumbTack,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import MainLayout from "@/layouts/MainLayout"; import MainLayout from "@/layouts/MainLayout";
import useLinkStore from "@/store/links"; import useLinkStore from "@/store/links";
import useTagStore from "@/store/tags"; import useTagStore from "@/store/tags";
import LinkCard from "@/components/LinkCard"; import LinkCard from "@/components/LinkCard";
import Link from "next/link";
import CollectionCard from "@/components/CollectionCard";
import { Disclosure, Transition } from "@headlessui/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useLinks from "@/hooks/useLinks"; import useLinks from "@/hooks/useLinks";
@ -21,20 +19,6 @@ export default function Dashboard() {
const [numberOfLinks, setNumberOfLinks] = useState(0); const [numberOfLinks, setNumberOfLinks] = useState(0);
const [tagPinDisclosure, setTagPinDisclosure] = useState<boolean>(() => {
const storedValue =
typeof window !== "undefined" && localStorage.getItem("tagPinDisclosure");
return storedValue ? storedValue === "true" : true;
});
const [collectionPinDisclosure, setCollectionPinDisclosure] =
useState<boolean>(() => {
const storedValue =
typeof window !== "undefined" &&
localStorage.getItem("collectionPinDisclosure");
return storedValue ? storedValue === "true" : true;
});
const [linkPinDisclosure, setLinkPinDisclosure] = useState<boolean>(() => { const [linkPinDisclosure, setLinkPinDisclosure] = useState<boolean>(() => {
const storedValue = const storedValue =
typeof window !== "undefined" && typeof window !== "undefined" &&
@ -54,20 +38,6 @@ export default function Dashboard() {
); );
}, [collections]); }, [collections]);
useEffect(() => {
localStorage.setItem(
"tagPinDisclosure",
tagPinDisclosure ? "true" : "false"
);
}, [tagPinDisclosure]);
useEffect(() => {
localStorage.setItem(
"collectionPinDisclosure",
collectionPinDisclosure ? "true" : "false"
);
}, [collectionPinDisclosure]);
useEffect(() => { useEffect(() => {
localStorage.setItem( localStorage.setItem(
"linkPinDisclosure", "linkPinDisclosure",
@ -76,183 +46,104 @@ export default function Dashboard() {
}, [linkPinDisclosure]); }, [linkPinDisclosure]);
return ( return (
// ml-80
<MainLayout> <MainLayout>
<div className="p-5"> <div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
<div className="flex gap-3 items-center mb-5"> <div className="flex gap-3 items-center">
<div className="flex gap-2"> <div className="flex gap-2">
<FontAwesomeIcon <FontAwesomeIcon
icon={faChartSimple} icon={faChartSimple}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 drop-shadow" className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
/> />
<p className="sm:text-4xl text-3xl capitalize text-sky-700 font-bold"> <p className="sm:text-4xl text-3xl text-black dark:text-white">
Dashboard Dashboard
</p> </p>
</div> </div>
</div> </div>
<br /> <div className="flex flex-col md:flex-row md:items-center gap-5">
<div className="sky-shadow flex flex-col justify-center items-center gap-2 md:w-full rounded-2xl p-10 border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800">
<div className="flex flex-col md:flex-row md:items-center justify-evenly gap-2 mb-10"> <p className="font-bold text-6xl text-sky-500 dark:text-sky-500">
<div className="flex items-baseline gap-2"> {numberOfLinks}
<p className="font-bold text-6xl text-sky-700">{numberOfLinks}</p> </p>
<p className="text-sky-900 text-xl"> <p className="text-black dark:text-white text-xl">
{numberOfLinks === 1 ? "Link" : "Links"} {numberOfLinks === 1 ? "Link" : "Links"}
</p> </p>
</div> </div>
<div className="flex items-baseline gap-2"> <div className="sky-shadow flex flex-col justify-center items-center gap-2 md:w-full rounded-2xl p-10 border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800">
<p className="font-bold text-6xl text-sky-700"> <p className="font-bold text-6xl text-sky-500 dark:text-sky-500">
{collections.length} {collections.length}
</p> </p>
<p className="text-sky-900 text-xl"> <p className="text-black dark:text-white text-xl">
{collections.length === 1 ? "Collection" : "Collections"} {collections.length === 1 ? "Collection" : "Collections"}
</p> </p>
</div> </div>
<div className="flex items-baseline gap-2"> <div className="sky-shadow flex flex-col justify-center items-center gap-2 md:w-full rounded-2xl p-10 border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800">
<p className="font-bold text-6xl text-sky-700">{tags.length}</p> <p className="font-bold text-6xl text-sky-500 dark:text-sky-500">
<p className="text-sky-900 text-xl"> {tags.length}
</p>
<p className="text-black dark:text-white text-xl">
{tags.length === 1 ? "Tag" : "Tags"} {tags.length === 1 ? "Tag" : "Tags"}
</p> </p>
</div> </div>
</div> </div>
{/* <hr className="my-5 border-sky-100" /> */} <div className="flex justify-between items-center">
<br /> <div className="flex gap-2 items-center">
<FontAwesomeIcon
<div className="flex flex-col 2xl:flex-row items-start justify-evenly 2xl:gap-2"> icon={faThumbTack}
className="w-5 h-5 text-sky-500 dark:text-sky-500 drop-shadow"
/>
<p className="text-2xl text-black dark:text-white">Pinned Links</p>
</div>
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? ( {links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
<Disclosure defaultOpen={linkPinDisclosure}> <button
<div className="flex flex-col gap-5 p-2 w-full mx-auto md:w-2/3"> className="text-black dark:text-white flex items-center gap-2 cursor-pointer"
<Disclosure.Button onClick={() => setLinkPinDisclosure(!linkPinDisclosure)}
onClick={() => {
setLinkPinDisclosure(!linkPinDisclosure);
}}
className="flex justify-between gap-2 items-baseline shadow active:shadow-inner duration-100 py-2 px-4 rounded-full"
> >
<p className="text-sky-700 text-xl">Pinned Links</p> {linkPinDisclosure ? "Show Less" : "Show More"}
<div className="text-sky-700 flex items-center gap-2">
{linkPinDisclosure ? "Hide" : "Show"}
<FontAwesomeIcon <FontAwesomeIcon
icon={faChevronDown} icon={faChevronDown}
className={`w-4 h-4 text-sky-300 ${ className={`w-4 h-4 text-black dark:text-white ${
linkPinDisclosure ? "rotate-reverse" : "rotate" linkPinDisclosure ? "rotate-reverse" : "rotate"
}`} }`}
/> />
</button>
) : undefined}
</div> </div>
</Disclosure.Button>
<Transition <div
enter="transition duration-100 ease-out" style={{ flex: "1 1 auto" }}
enterFrom="transform opacity-0 -translate-y-3" className="flex flex-col 2xl:flex-row items-start justify-evenly 2xl:gap-2"
enterTo="transform opacity-100 translate-y-0" >
leave="transition duration-100 ease-out" {links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
leaveFrom="transform opacity-100 translate-y-0" <div
leaveTo="transform opacity-0 -translate-y-3" className={`grid overflow-hidden 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full ${
linkPinDisclosure ? "h-full" : "h-44"
}`}
> >
<Disclosure.Panel className="grid grid-cols-1 xl:grid-cols-2 gap-5 w-full">
{links {links
.filter((e) => e.pinnedBy && e.pinnedBy[0]) .filter((e) => e.pinnedBy && e.pinnedBy[0])
.map((e, i) => ( .map((e, i) => (
<LinkCard key={i} link={e} count={i} /> <LinkCard key={i} link={e} count={i} />
))} ))}
</Disclosure.Panel>
</Transition>
</div> </div>
</Disclosure>
) : ( ) : (
<div className="border border-solid border-sky-100 w-full mx-auto md:w-2/3 p-10 rounded-2xl"> <div
<p className="text-center text-2xl text-sky-700"> style={{ flex: "1 1 auto" }}
No Pinned Links className="sky-shadow flex flex-col justify-center h-full border border-solid border-sky-100 dark:border-neutral-700 w-full mx-auto p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"
>
<p className="text-center text-2xl text-black dark:text-white">
Pin Your Favorite Links Here!
</p> </p>
<p className="text-center text-sky-900 text-sm"> <p className="text-center mx-auto max-w-96 w-fit text-gray-500 dark:text-gray-300 text-sm mt-2">
You can Pin Links by clicking on the three dots on each Link and You can Pin your favorite Links by clicking on the three dots on
clicking &quot;Pin to Dashboard.&quot; each Link and clicking{" "}
<span className="font-semibold">Pin to Dashboard</span>.
</p> </p>
</div> </div>
)} )}
{/* <Disclosure defaultOpen={collectionPinDisclosure}>
<div className="flex flex-col gap-5 p-2 w-full">
<Disclosure.Button
onClick={() => {
setCollectionPinDisclosure(!collectionPinDisclosure);
}}
className="flex justify-between gap-2 items-baseline shadow active:shadow-inner duration-100 py-2 px-4 rounded-full"
>
<p className="text-sky-700 text-xl">Pinned Collections</p>
<div className="text-sky-700 flex items-center gap-2">
{collectionPinDisclosure ? "Hide" : "Show"}
<FontAwesomeIcon
icon={faChevronDown}
className={`w-4 h-4 text-sky-300 ${
collectionPinDisclosure ? "rotate-reverse" : "rotate"
}`}
/>
</div>
</Disclosure.Button>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0 -translate-y-3"
enterTo="transform opacity-100 translate-y-0"
leave="transition duration-100 ease-out"
leaveFrom="transform opacity-100 translate-y-0"
leaveTo="transform opacity-0 -translate-y-3"
>
<Disclosure.Panel className="flex flex-col gap-5 w-full">
{collections.slice(0, 5).map((e, i) => (
<CollectionCard key={i} collection={e} />
))}
</Disclosure.Panel>
</Transition>
</div>
</Disclosure> */}
{/* <Disclosure defaultOpen={tagPinDisclosure}>
<div className="flex flex-col gap-5 p-2 w-full">
<Disclosure.Button
onClick={() => {
setTagPinDisclosure(!tagPinDisclosure);
}}
className="flex justify-between gap-2 items-baseline shadow active:shadow-inner duration-100 py-2 px-4 rounded-full"
>
<p className="text-sky-700 text-xl">Pinned Tags</p>
<div className="text-sky-700 flex items-center gap-2">
{tagPinDisclosure ? "Hide" : "Show"}
<FontAwesomeIcon
icon={faChevronDown}
className={`w-4 h-4 text-sky-300 ${
tagPinDisclosure ? "rotate-reverse" : "rotate"
}`}
/>
</div>
</Disclosure.Button>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0 -translate-y-3"
enterTo="transform opacity-100 translate-y-0"
leave="transition duration-100 ease-out"
leaveFrom="transform opacity-100 translate-y-0"
leaveTo="transform opacity-0 -translate-y-3"
>
<Disclosure.Panel className="flex gap-2 flex-wrap">
{tags.slice(0, 19).map((e, i) => (
<Link
href={`/tags/${e.id}`}
key={i}
className="px-2 py-1 bg-sky-200 rounded-full hover:opacity-60 duration-100 text-sky-700"
>
{e.name}
</Link>
))}
</Disclosure.Panel>
</Transition>
</div>
</Disclosure> */}
</div> </div>
</div> </div>
</MainLayout> </MainLayout>

View File

@ -1,4 +1,5 @@
import SubmitButton from "@/components/SubmitButton"; import SubmitButton from "@/components/SubmitButton";
import TextInput from "@/components/TextInput";
import CenteredForm from "@/layouts/CenteredForm"; import CenteredForm from "@/layouts/CenteredForm";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import Image from "next/image"; import Image from "next/image";
@ -40,27 +41,31 @@ export default function Forgot() {
return ( return (
<CenteredForm> <CenteredForm>
<div className="p-2 flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 rounded-2xl shadow-md border border-sky-100"> <div className="p-4 flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 dark:border-neutral-700 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
<p className="text-2xl text-black font-bold">Password Recovery</p> <p className="text-2xl text-center text-black dark:text-white font-bold">
Password Recovery
</p>
<div> <div>
<p className="text-md text-black"> <p className="text-md text-black dark:text-white">
Enter your Email so we can send you a link to recover your account. Enter your Email so we can send you a link to recover your account.
Make sure to change your password in the profile settings Make sure to change your password in the profile settings
afterwards. afterwards.
</p> </p>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500 dark:text-gray-400">
You wont get logged in if you haven&apos;t created an account yet. You wont get logged in if you haven&apos;t created an account yet.
</p> </p>
</div> </div>
<div> <div>
<p className="text-sm text-sky-700 w-fit font-semibold mb-1">Email</p> <p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Email
</p>
<input <TextInput
type="text" type="email"
placeholder="johnny@example.com" placeholder="johnny@example.com"
value={form.email} value={form.email}
className="bg-white"
onChange={(e) => setForm({ ...form, email: e.target.value })} onChange={(e) => setForm({ ...form, email: e.target.value })}
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
@ -71,7 +76,10 @@ export default function Forgot() {
loading={submitLoader} loading={submitLoader}
/> />
<div className="flex items-baseline gap-1 justify-center"> <div className="flex items-baseline gap-1 justify-center">
<Link href={"/login"} className="block text-sky-700 font-bold"> <Link
href={"/login"}
className="block text-black dark:text-white font-bold"
>
Go back Go back
</Link> </Link>
</div> </div>

View File

@ -24,9 +24,9 @@ export default function Links() {
<div className="flex gap-2"> <div className="flex gap-2">
<FontAwesomeIcon <FontAwesomeIcon
icon={faLink} icon={faLink}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 drop-shadow" className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
/> />
<p className="sm:text-4xl text-3xl capitalize text-sky-700 font-bold"> <p className="sm:text-4xl text-3xl capitalize text-black dark:text-white">
All Links All Links
</p> </p>
</div> </div>
@ -35,12 +35,12 @@ export default function Links() {
<div <div
onClick={() => setSortDropdown(!sortDropdown)} onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown" id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 duration-100 p-1" className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faSort} icon={faSort}
id="sort-dropdown" id="sort-dropdown"
className="w-5 h-5 text-gray-500" className="w-5 h-5 text-gray-500 dark:text-gray-300"
/> />
</div> </div>

View File

@ -1,4 +1,5 @@
import SubmitButton from "@/components/SubmitButton"; import SubmitButton from "@/components/SubmitButton";
import TextInput from "@/components/TextInput";
import CenteredForm from "@/layouts/CenteredForm"; import CenteredForm from "@/layouts/CenteredForm";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import Image from "next/image"; import Image from "next/image";
@ -47,41 +48,43 @@ export default function Login() {
return ( return (
<CenteredForm text="Sign in to your account"> <CenteredForm text="Sign in to your account">
<div className="p-2 flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 rounded-2xl shadow-md border border-sky-100"> <div className="p-4 flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
<p className="text-2xl text-black text-center font-bold"> <p className="text-2xl text-black dark:text-white text-center font-bold">
Enter your credentials Enter your credentials
</p> </p>
<div> <div>
<p className="text-sm text-sky-700 w-fit font-semibold mb-1"> <p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Username Username
{emailEnabled ? "/Email" : undefined} {emailEnabled ? " or Email" : undefined}
</p> </p>
<input <TextInput
type="text"
placeholder="johnny" placeholder="johnny"
value={form.username} value={form.username}
className="bg-white"
onChange={(e) => setForm({ ...form, username: e.target.value })} onChange={(e) => setForm({ ...form, username: e.target.value })}
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
<div> <div>
<p className="text-sm text-sky-700 w-fit font-semibold mb-1"> <p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Password Password
</p> </p>
<input <TextInput
type="password" type="password"
placeholder="••••••••••••••" placeholder="••••••••••••••"
value={form.password} value={form.password}
className="bg-white"
onChange={(e) => setForm({ ...form, password: e.target.value })} onChange={(e) => setForm({ ...form, password: e.target.value })}
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
{emailEnabled && ( {emailEnabled && (
<div className="w-fit ml-auto mt-1"> <div className="w-fit ml-auto mt-1">
<Link href={"/forgot"} className="text-gray-500 font-semibold"> <Link
href={"/forgot"}
className="text-gray-500 dark:text-gray-400 font-semibold"
>
Forgot Password? Forgot Password?
</Link> </Link>
</div> </div>
@ -95,8 +98,11 @@ export default function Login() {
loading={submitLoader} loading={submitLoader}
/> />
<div className="flex items-baseline gap-1 justify-center"> <div className="flex items-baseline gap-1 justify-center">
<p className="w-fit text-gray-500">New here?</p> <p className="w-fit text-gray-500 dark:text-gray-400">New here?</p>
<Link href={"/register"} className="block text-sky-700 font-bold"> <Link
href={"/register"}
className="block text-black dark:text-white font-semibold"
>
Sign Up Sign Up
</Link> </Link>
</div> </div>

View File

@ -11,6 +11,8 @@ export default function PublicCollections() {
const [data, setData] = useState<PublicCollectionIncludingLinks>(); const [data, setData] = useState<PublicCollectionIncludingLinks>();
document.body.style.background = "white";
useEffect(() => { useEffect(() => {
if (router.query.id) { if (router.query.id) {
getPublicCollectionData( getPublicCollectionData(
@ -45,9 +47,7 @@ export default function PublicCollections() {
<div <div
className={`text-center bg-gradient-to-tr from-sky-100 from-10% via-gray-100 via-20% rounded-3xl shadow-lg p-5`} className={`text-center bg-gradient-to-tr from-sky-100 from-10% via-gray-100 via-20% rounded-3xl shadow-lg p-5`}
> >
<p className="text-5xl text-sky-700 font-bold mb-5 capitalize"> <p className="text-5xl text-black mb-5 capitalize">{data.name}</p>
{data.name}
</p>
{data.description && ( {data.description && (
<> <>
@ -64,7 +64,7 @@ export default function PublicCollections() {
</div> </div>
{/* <p className="text-center font-bold text-gray-500"> {/* <p className="text-center font-bold text-gray-500">
List created with <span className="text-sky-700">Linkwarden.</span> List created with <span className="text-black">Linkwarden.</span>
</p> */} </p> */}
</div> </div>
) : ( ) : (

View File

@ -5,6 +5,7 @@ import SubmitButton from "@/components/SubmitButton";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import CenteredForm from "@/layouts/CenteredForm"; import CenteredForm from "@/layouts/CenteredForm";
import TextInput from "@/components/TextInput";
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
@ -98,107 +99,108 @@ export default function Register() {
: "Create a new account" : "Create a new account"
} }
> >
<div className="p-2 flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 rounded-2xl shadow-md border border-sky-100"> <div className="p-4 flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
<p className="text-2xl text-black text-center font-bold"> <p className="text-2xl text-black dark:text-white text-center font-bold">
Enter your details Enter your details
</p> </p>
<div> <div>
<p className="text-sm text-sky-700 w-fit font-semibold mb-1"> <p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Display Name Display Name
</p> </p>
<input <TextInput
type="text"
placeholder="Johnny" placeholder="Johnny"
value={form.name} value={form.name}
className="bg-white"
onChange={(e) => setForm({ ...form, name: e.target.value })} onChange={(e) => setForm({ ...form, name: e.target.value })}
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
{emailEnabled ? undefined : ( {emailEnabled ? undefined : (
<div> <div>
<p className="text-sm text-sky-700 w-fit font-semibold mb-1"> <p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Username Username
</p> </p>
<input <TextInput
type="text"
placeholder="john" placeholder="john"
value={form.username} value={form.username}
className="bg-white"
onChange={(e) => setForm({ ...form, username: e.target.value })} onChange={(e) => setForm({ ...form, username: e.target.value })}
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
)} )}
{emailEnabled ? ( {emailEnabled ? (
<div> <div>
<p className="text-sm text-sky-700 w-fit font-semibold mb-1"> <p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Email Email
</p> </p>
<input <TextInput
type="email" type="email"
placeholder="johnny@example.com" placeholder="johnny@example.com"
value={form.email} value={form.email}
className="bg-white"
onChange={(e) => setForm({ ...form, email: e.target.value })} onChange={(e) => setForm({ ...form, email: e.target.value })}
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
) : undefined} ) : undefined}
<div className="w-full"> <div className="w-full">
<p className="text-sm text-sky-700 w-fit font-semibold mb-1"> <p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Password Password
</p> </p>
<input <TextInput
type="password" type="password"
placeholder="••••••••••••••" placeholder="••••••••••••••"
value={form.password} value={form.password}
className="bg-white"
onChange={(e) => setForm({ ...form, password: e.target.value })} onChange={(e) => setForm({ ...form, password: e.target.value })}
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
<div className="w-full"> <div className="w-full">
<p className="text-sm text-sky-700 w-fit font-semibold mb-1"> <p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Confirm Password Confirm Password
</p> </p>
<input <TextInput
type="password" type="password"
placeholder="••••••••••••••" placeholder="••••••••••••••"
value={form.passwordConfirmation} value={form.passwordConfirmation}
className="bg-white"
onChange={(e) => onChange={(e) =>
setForm({ ...form, passwordConfirmation: e.target.value }) setForm({ ...form, passwordConfirmation: e.target.value })
} }
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-700 duration-100"
/> />
</div> </div>
{process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE ? ( {process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE ? (
<div> <div>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500 dark:text-gray-400">
By signing up, you agree to our{" "} By signing up, you agree to our{" "}
<Link href="https://linkwarden.app/tos" className="font-semibold"> <Link
href="https://linkwarden.app/tos"
className="font-semibold underline"
>
Terms of Service Terms of Service
</Link>{" "} </Link>{" "}
and{" "} and{" "}
<Link <Link
href="https://linkwarden.app/privacy-policy" href="https://linkwarden.app/privacy-policy"
className="font-semibold" className="font-semibold underline"
> >
Privacy Policy Privacy Policy
</Link> </Link>
. .
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500 dark:text-gray-400">
Need help?{" "} Need help?{" "}
<Link <Link
href="mailto:support@linkwarden.app" href="mailto:support@linkwarden.app"
className="font-semibold" className="font-semibold underline"
> >
Get in touch Get in touch
</Link> </Link>
@ -214,8 +216,13 @@ export default function Register() {
loading={submitLoader} loading={submitLoader}
/> />
<div className="flex items-baseline gap-1 justify-center"> <div className="flex items-baseline gap-1 justify-center">
<p className="w-fit text-gray-500">Already have an account?</p> <p className="w-fit text-gray-500 dark:text-gray-400">
<Link href={"/login"} className="block text-sky-700 font-bold"> Already have an account?
</p>
<Link
href={"/login"}
className="block text-black dark:text-white font-bold"
>
Login Login
</Link> </Link>
</div> </div>

View File

@ -40,9 +40,9 @@ export default function Links() {
<div className="flex gap-2"> <div className="flex gap-2">
<FontAwesomeIcon <FontAwesomeIcon
icon={faSearch} icon={faSearch}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 drop-shadow" className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
/> />
<p className="sm:text-4xl text-3xl capitalize text-sky-700 font-bold"> <p className="sm:text-4xl text-3xl capitalize text-black dark:text-white">
Search Results Search Results
</p> </p>
</div> </div>
@ -53,12 +53,12 @@ export default function Links() {
<div <div
onClick={() => setFilterDropdown(!filterDropdown)} onClick={() => setFilterDropdown(!filterDropdown)}
id="filter-dropdown" id="filter-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 duration-100 p-1" className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faFilter} icon={faFilter}
id="filter-dropdown" id="filter-dropdown"
className="w-5 h-5 text-gray-500" className="w-5 h-5 text-gray-500 dark:text-gray-300"
/> />
</div> </div>
@ -75,12 +75,12 @@ export default function Links() {
<div <div
onClick={() => setSortDropdown(!sortDropdown)} onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown" id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 duration-100 p-1" className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faSort} icon={faSort}
id="sort-dropdown" id="sort-dropdown"
className="w-5 h-5 text-gray-500" className="w-5 h-5 text-gray-500 dark:text-gray-300"
/> />
</div> </div>
@ -95,13 +95,15 @@ export default function Links() {
</div> </div>
</div> </div>
{links[0] ? ( {links[0] ? (
links.map((e, i) => { <div className="grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5">
{links.map((e, i) => {
return <LinkCard key={i} link={e} count={i} />; return <LinkCard key={i} link={e} count={i} />;
}) })}
</div>
) : ( ) : (
<p className="text-sky-900"> <p className="text-black dark:text-white">
Nothing found.{" "} Nothing found.{" "}
<span className="text-sky-700 font-bold text-xl" title="Shruggie"> <span className="font-bold text-xl" title="Shruggie">
¯\_()_/¯ ¯\_()_/¯
</span> </span>
</p> </p>

View File

@ -6,10 +6,13 @@ import { toast } from "react-hot-toast";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import CenteredForm from "@/layouts/CenteredForm"; import CenteredForm from "@/layouts/CenteredForm";
import { Plan } from "@/types/global";
export default function Subscribe() { export default function Subscribe() {
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
const [plan, setPlan] = useState<Plan>(0);
const { data, status } = useSession(); const { data, status } = useSession();
const router = useRouter(); const router = useRouter();
@ -18,7 +21,7 @@ export default function Subscribe() {
const redirectionToast = toast.loading("Redirecting to Stripe..."); const redirectionToast = toast.loading("Redirecting to Stripe...");
const res = await fetch("/api/payment"); const res = await fetch("/api/payment?plan=" + plan);
const data = await res.json(); const data = await res.json();
router.push(data.response); router.push(data.response);
@ -26,27 +29,74 @@ export default function Subscribe() {
return ( return (
<CenteredForm <CenteredForm
text={`${ text={`Start with a ${
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14 process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
}-Day free trial, then $ }-day free trial, cancel anytime!`}
${process.env.NEXT_PUBLIC_PRICING}/month afterwards`}
> >
<div className="p-2 mx-auto flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 rounded-2xl shadow-md border border-sky-100"> <div className="p-2 mx-auto flex flex-col gap-3 justify-between sm:w-[30rem] dark:border-neutral-700 w-80 bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
<p className="text-2xl text-center font-bold"> <p className="text-2xl text-center font-bold">
Subscribe to Linkwarden! Subscribe to Linkwarden!
</p> </p>
<div> <div>
<p>You will be redirected to Stripe.</p>
<p> <p>
Feel free to reach out to us at{" "} You will be redirected to Stripe, feel free to reach out to us at{" "}
<a className="font-semibold" href="mailto:support@linkwarden.app"> <a className="font-semibold" href="mailto:support@linkwarden.app">
support@linkwarden.app support@linkwarden.app
</a>{" "} </a>{" "}
in case of any issues. in case of any issue.
</p> </p>
</div> </div>
<div className="flex text-white dark:text-black gap-3 border border-solid border-sky-100 dark:border-neutral-700 w-4/5 mx-auto p-1 rounded-xl relative">
<button
onClick={() => setPlan(Plan.monthly)}
className={`w-full text-black dark:text-white duration-75 text-sm rounded-lg p-1 ${
plan === Plan.monthly
? "text-white bg-sky-700 dark:bg-sky-700"
: "hover:opacity-80"
}`}
>
<p>Monthly</p>
</button>
<button
onClick={() => setPlan(Plan.yearly)}
className={`w-full text-black dark:text-white duration-75 text-sm rounded-lg p-1 ${
plan === Plan.yearly
? "text-white bg-sky-700 dark:bg-sky-700"
: "hover:opacity-80"
}`}
>
<p>Yearly</p>
</button>
<div className="absolute -top-4 -right-4 px-1 bg-red-500 text-white rounded-md rotate-[22deg]">
%25 Off
</div>
</div>
<div className="flex flex-col gap-2 justify-center items-center">
<p className="text-3xl">
${plan === Plan.monthly ? "4" : "3"}
<span className="text-base text-gray-500 dark:text-gray-400">
/mo
</span>
</p>
<p className="font-semibold">
Billed {plan === Plan.monthly ? "Monthly" : "Yearly"}
</p>
<div className="flex gap-3">
<p className="w-fit">Total:</p>
<div className="w-full p-1 rounded-md border border-solid border-sky-100 dark:border-neutral-700">
<p className="text-sm">
{process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS}-day free trial, then
${plan === Plan.monthly ? "4" : "3"} per month
</p>
<p className="text-sm">+ VAT if applicable</p>
</div>
</div>
</div>
<SubmitButton <SubmitButton
onClick={loginUser} onClick={loginUser}
label="Complete your Subscription" label="Complete your Subscription"
@ -56,7 +106,7 @@ export default function Subscribe() {
<div <div
onClick={() => signOut()} onClick={() => signOut()}
className="w-fit mx-auto cursor-pointer text-gray-500 font-semibold " className="w-fit mx-auto cursor-pointer text-gray-500 dark:text-gray-400 font-semibold "
> >
Sign Out Sign Out
</div> </div>

View File

@ -36,9 +36,9 @@ export default function Index() {
<div className="flex gap-2"> <div className="flex gap-2">
<FontAwesomeIcon <FontAwesomeIcon
icon={faHashtag} icon={faHashtag}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500" className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500"
/> />
<p className="sm:text-4xl text-3xl capitalize text-sky-700 font-bold"> <p className="sm:text-4xl text-3xl capitalize text-black dark:text-white">
{activeTag?.name} {activeTag?.name}
</p> </p>
</div> </div>
@ -48,12 +48,12 @@ export default function Index() {
<div <div
onClick={() => setSortDropdown(!sortDropdown)} onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown" id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 duration-100 p-1" className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faSort} icon={faSort}
id="sort-dropdown" id="sort-dropdown"
className="w-5 h-5 text-gray-500" className="w-5 h-5 text-gray-500 dark:text-gray-300"
/> />
</div> </div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 671 B

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -15,7 +15,7 @@ type AccountStore = {
const useAccountStore = create<AccountStore>()((set) => ({ const useAccountStore = create<AccountStore>()((set) => ({
account: {} as AccountSettings, account: {} as AccountSettings,
setAccount: async (id) => { setAccount: async (id) => {
const response = await fetch(`/api/routes/users?id=${id}`); const response = await fetch(`/api/users?id=${id}`);
const data = await response.json(); const data = await response.json();
@ -24,7 +24,7 @@ const useAccountStore = create<AccountStore>()((set) => ({
if (response.ok) set({ account: { ...data.response, profilePic } }); if (response.ok) set({ account: { ...data.response, profilePic } });
}, },
updateAccount: async (user) => { updateAccount: async (user) => {
const response = await fetch("/api/routes/users", { const response = await fetch("/api/users", {
method: "PUT", method: "PUT",
body: JSON.stringify(user), body: JSON.stringify(user),
headers: { headers: {

View File

@ -22,14 +22,14 @@ type CollectionStore = {
const useCollectionStore = create<CollectionStore>()((set) => ({ const useCollectionStore = create<CollectionStore>()((set) => ({
collections: [], collections: [],
setCollections: async () => { setCollections: async () => {
const response = await fetch("/api/routes/collections"); const response = await fetch("/api/collections");
const data = await response.json(); const data = await response.json();
if (response.ok) set({ collections: data.response }); if (response.ok) set({ collections: data.response });
}, },
addCollection: async (body) => { addCollection: async (body) => {
const response = await fetch("/api/routes/collections", { const response = await fetch("/api/collections", {
body: JSON.stringify(body), body: JSON.stringify(body),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -47,7 +47,7 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
return { ok: response.ok, data: data.response }; return { ok: response.ok, data: data.response };
}, },
updateCollection: async (collection) => { updateCollection: async (collection) => {
const response = await fetch("/api/routes/collections", { const response = await fetch("/api/collections", {
body: JSON.stringify(collection), body: JSON.stringify(collection),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -67,7 +67,7 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
return { ok: response.ok, data: data.response }; return { ok: response.ok, data: data.response };
}, },
removeCollection: async (id) => { removeCollection: async (id) => {
const response = await fetch("/api/routes/collections", { const response = await fetch("/api/collections", {
body: JSON.stringify({ id }), body: JSON.stringify({ id }),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@ -38,7 +38,7 @@ const useLinkStore = create<LinkStore>()((set) => ({
})); }));
}, },
addLink: async (body) => { addLink: async (body) => {
const response = await fetch("/api/routes/links", { const response = await fetch("/api/links", {
body: JSON.stringify(body), body: JSON.stringify(body),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -59,7 +59,7 @@ const useLinkStore = create<LinkStore>()((set) => ({
return { ok: response.ok, data: data.response }; return { ok: response.ok, data: data.response };
}, },
updateLink: async (link) => { updateLink: async (link) => {
const response = await fetch("/api/routes/links", { const response = await fetch("/api/links", {
body: JSON.stringify(link), body: JSON.stringify(link),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -82,7 +82,7 @@ const useLinkStore = create<LinkStore>()((set) => ({
return { ok: response.ok, data: data.response }; return { ok: response.ok, data: data.response };
}, },
removeLink: async (link) => { removeLink: async (link) => {
const response = await fetch("/api/routes/links", { const response = await fetch("/api/links", {
body: JSON.stringify(link), body: JSON.stringify(link),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@ -9,7 +9,7 @@ type TagStore = {
const useTagStore = create<TagStore>()((set) => ({ const useTagStore = create<TagStore>()((set) => ({
tags: [], tags: [],
setTags: async () => { setTags: async () => {
const response = await fetch("/api/routes/tags"); const response = await fetch("/api/tags");
const data = await response.json(); const data = await response.json();

View File

@ -47,6 +47,21 @@
animation: slide-up-animation 70ms; animation: slide-up-animation 70ms;
} }
.slide-down {
animation: slide-down-animation 70ms;
}
@keyframes slide-down-animation {
0% {
transform: translateY(-15%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slide-up-animation { @keyframes slide-up-animation {
0% { 0% {
transform: translateY(15%); transform: translateY(15%);
@ -136,11 +151,6 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
pointer-events: none; pointer-events: none;
background-image: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 1) 90%
);
width: 100%; width: 100%;
height: 4rem; height: 4rem;
} }
@ -151,11 +161,73 @@
top: 0; top: 0;
left: 0; left: 0;
pointer-events: none; pointer-events: none;
width: 100%;
height: 4rem;
}
/* For light mode */
.banner-light-mode .link-banner::after {
background-image: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 1) 90%
);
}
.banner-light-mode .link-banner::before {
background-image: linear-gradient( background-image: linear-gradient(
to top, to top,
rgba(255, 255, 255, 0), rgba(255, 255, 255, 0),
rgba(255, 255, 255, 1) 90% rgba(255, 255, 255, 1) 90%
); );
width: 100%; }
height: 4rem;
/* For dark mode */
.banner-dark-mode .link-banner::after {
background-image: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
#171717 90%
);
}
.banner-dark-mode .link-banner::before {
background-image: linear-gradient(
to top,
rgba(255, 255, 255, 0),
#171717 90%
);
}
/* Theme */
@layer base {
body {
@apply dark:bg-neutral-900 bg-white dark:text-white;
}
}
/* react-select */
@layer components {
.react-select-container .react-select__control {
@apply dark:bg-neutral-950 bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-500;
}
.react-select-container {
@apply dark:border-neutral-700;
}
.react-select-container .react-select__menu {
@apply dark:bg-neutral-900 dark:border-neutral-700 border;
}
.react-select-container .react-select__option {
@apply dark:hover:bg-neutral-800;
}
.react-select-container .react-select__input-container,
.react-select-container .react-select__single-value {
@apply dark:text-white;
}
}
.sky-shadow {
box-shadow: 0px 0px 3px #0ea5e9;
} }

View File

@ -1,6 +1,7 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: "class",
content: [ content: [
"./app/**/*.{js,ts,jsx,tsx}", "./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}", "./pages/**/*.{js,ts,jsx,tsx}",
@ -9,8 +10,5 @@ module.exports = {
// For the "layouts" directory // For the "layouts" directory
"./layouts/**/*.{js,ts,jsx,tsx}", "./layouts/**/*.{js,ts,jsx,tsx}",
], ],
theme: {
extend: {},
},
plugins: [], plugins: [],
}; };

View File

@ -20,11 +20,11 @@ declare global {
NEXT_PUBLIC_STRIPE_IS_ACTIVE?: string; NEXT_PUBLIC_STRIPE_IS_ACTIVE?: string;
STRIPE_SECRET_KEY?: string; STRIPE_SECRET_KEY?: string;
PRICE_ID?: string; MONTHLY_PRICE_ID?: string;
YEARLY_PRICE_ID?: string;
NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL?: string; NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL?: string;
NEXT_PUBLIC_TRIAL_PERIOD_DAYS?: string; NEXT_PUBLIC_TRIAL_PERIOD_DAYS?: string;
BASE_URL?: string; BASE_URL?: string;
NEXT_PUBLIC_PRICING?: string;
} }
} }
} }

View File

@ -85,3 +85,8 @@ interface CollectionIncludingLinks extends Collection {
export interface Backup extends Omit<User, "password" | "id" | "image"> { export interface Backup extends Omit<User, "password" | "id" | "image"> {
collections: CollectionIncludingLinks[]; collections: CollectionIncludingLinks[];
} }
export enum Plan {
monthly,
yearly,
}

View File

@ -3558,6 +3558,11 @@ next-auth@^4.22.1:
preact-render-to-string "^5.1.19" preact-render-to-string "^5.1.19"
uuid "^8.3.2" uuid "^8.3.2"
next-themes@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.2.1.tgz#0c9f128e847979daf6c67f70b38e6b6567856e45"
integrity sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==
next@13.4.12: next@13.4.12:
version "13.4.12" version "13.4.12"
resolved "https://registry.yarnpkg.com/next/-/next-13.4.12.tgz#809b21ea0aabbe88ced53252c88c4a5bd5af95df" resolved "https://registry.yarnpkg.com/next/-/next-13.4.12.tgz#809b21ea0aabbe88ced53252c88c4a5bd5af95df"