Merge pull request #58 from linkwarden/dev

Dev
This commit is contained in:
Daniel 2023-07-14 16:06:09 -04:00 committed by GitHub
commit 5e40f5dd44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 242 additions and 225 deletions

View File

@ -80,16 +80,16 @@ export default function ChangePassword({
value={newPassword}
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-500 duration-100"
/>
<p className="text-sm text-sky-500">Re-enter New Password</p>
<p className="text-sm text-sky-500">Confirm New Password</p>
<input
value={newPassword2}
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-500 duration-100"
/>

View File

@ -2,7 +2,6 @@ import useCollectionStore from "@/store/collections";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faFolder,
faBox,
faHashtag,
faChartSimple,
faChevronDown,
@ -13,6 +12,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { Disclosure, Transition } from "@headlessui/react";
import Image from "next/image";
export default function Sidebar({ className }: { className?: string }) {
const [tagDisclosure, setTagDisclosure] = useState<boolean>(() => {
@ -53,55 +53,50 @@ export default function Sidebar({ className }: { className?: string }) {
<div
className={`bg-gray-100 h-screen w-64 xl:w-80 overflow-y-auto border-solid border-r-sky-100 px-2 border z-20 ${className}`}
>
<p className="p-2 text-sky-500 font-bold text-2xl my-2 leading-4">
Linkwarden
</p>
<div className="flex flex-col gap-1">
<Link href="/dashboard">
<div
<div className="flex justify-center gap-2 mt-2">
<Link
href="/dashboard"
className={`${
active === "/dashboard"
? "bg-sky-200"
: "hover:bg-slate-200 bg-gray-100"
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex items-center gap-2`}
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex justify-center flex-col items-center gap-1`}
>
<FontAwesomeIcon
icon={faChartSimple}
className={`w-6 h-6 drop-shadow text-sky-500`}
className={`w-8 h-8 drop-shadow text-sky-500`}
/>
<p className="text-sky-600">Dashboard</p>
</div>
<p className="text-sky-600 text-xs font-semibold">Dashboard</p>
</Link>
<Link href="/links">
<div
<Link
href="/links"
className={`${
active === "/links"
? "bg-sky-200"
: "hover:bg-slate-200 bg-gray-100"
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex items-center gap-2`}
} 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
icon={faLink}
className={`w-6 h-6 drop-shadow text-sky-500`}
className={`w-8 h-8 drop-shadow text-sky-500`}
/>
<p className="text-sky-600">All Links</p>
</div>
<p className="text-sky-600 text-xs font-semibold">All Links</p>
</Link>
<Link href="/collections">
<div
<Link
href="/collections"
className={`${
active === "/collections" ? "bg-sky-200" : "hover:bg-slate-200"
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex items-center gap-2`}
} 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
icon={faBox}
className={`w-6 h-6 drop-shadow text-sky-500`}
icon={faFolder}
className={`w-8 h-8 drop-shadow text-sky-500`}
/>
<p className="text-sky-600">All Collections</p>
</div>
<p className="text-sky-600 text-xs font-semibold">
<span className="hidden xl:inline-block">All</span> Collections
</p>
</Link>
</div>

View File

@ -20,14 +20,22 @@ export default function AuthRedirect({ children }: Props) {
if (!router.pathname.startsWith("/public")) {
if (
status === "authenticated" &&
(router.pathname === "/login" || router.pathname === "/register")
(router.pathname === "/login" ||
router.pathname === "/register" ||
router.pathname === "/confirmation" ||
router.pathname === "/forgot")
) {
router.push("/").then(() => {
setRedirect(false);
});
} else if (
status === "unauthenticated" &&
!(router.pathname === "/login" || router.pathname === "/register")
!(
router.pathname === "/login" ||
router.pathname === "/register" ||
router.pathname === "/confirmation" ||
router.pathname === "/forgot"
)
) {
router.push("/login").then(() => {
setRedirect(false);

View File

@ -5,8 +5,6 @@ import { createTransport } from "nodemailer";
export default async function sendVerificationRequest(
params: SendVerificationRequestParams
) {
console.log(params);
const { identifier, url, provider, theme } = params;
const { host } = new URL(url);
const transport = createTransport(provider.server);

View File

@ -19,7 +19,7 @@ const addMemberToCollection = async (
// member can't be empty
memberUsername.trim() !== "" &&
// member can't be the owner
memberUsername.trim() !== ownerUsername
memberUsername.trim().toLowerCase() !== ownerUsername.toLowerCase()
) {
// Lookup, get data/err, list ...
const user = await getPublicUserDataByUsername(
@ -40,7 +40,7 @@ const addMemberToCollection = async (
});
}
} else if (checkIfMemberAlreadyExists) toast.error("User already exists.");
else if (memberUsername.trim() === ownerUsername)
else if (memberUsername.trim().toLowerCase() === ownerUsername.toLowerCase())
toast.error("You are already the collection owner.");
};

View File

@ -27,7 +27,6 @@
"@types/nodemailer": "^6.4.8",
"@types/react": "18.2.14",
"@types/react-dom": "18.2.6",
"bcrypt": "^5.1.0",
"colorthief": "^2.4.0",
"crypto-js": "^4.1.1",

View File

@ -10,6 +10,8 @@ import { Adapter } from "next-auth/adapters";
import sendVerificationRequest from "@/lib/api/sendVerificationRequest";
import { Provider } from "next-auth/providers";
let email;
const providers: Provider[] = [
CredentialsProvider({
type: "credentials",
@ -63,6 +65,7 @@ if (process.env.EMAIL_SERVER && process.env.EMAIL_FROM)
from: process.env.EMAIL_FROM,
maxAge: 600,
sendVerificationRequest(params) {
email = params.identifier;
sendVerificationRequest(params);
},
})
@ -76,6 +79,7 @@ export const authOptions: AuthOptions = {
providers,
pages: {
signIn: "/login",
verifyRequest: "/confirmation",
},
callbacks: {
session: async ({ session, token }: { session: Session; token: JWT }) => {

View File

@ -1,7 +1,7 @@
import useCollectionStore from "@/store/collections";
import {
faBox,
faEllipsis,
faFolder,
faPlus,
faSort,
} from "@fortawesome/free-solid-svg-icons";
@ -36,7 +36,7 @@ export default function Collections() {
<div className="flex gap-3 items-end">
<div className="flex gap-2">
<FontAwesomeIcon
icon={faBox}
icon={faFolder}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 drop-shadow"
/>
<p className="sm:text-4xl text-3xl capitalize bg-gradient-to-tr from-sky-500 to-slate-400 bg-clip-text text-transparent font-bold">

View File

@ -1,23 +1,24 @@
import { signIn } from "next-auth/react";
import React from "react";
export default function EmailConfirmaion({ email }: { email: string }) {
export default function EmailConfirmaion() {
return (
<div className="overflow-y-auto py-2 fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
<div className="mx-auto p-3 rounded-xl border border-sky-100 shadow-lg bg-gray-100 text-sky-800">
<div className="mx-auto p-3 text-center rounded-xl border border-sky-100 shadow-lg bg-gray-100 text-sky-800">
<p className="text-center text-2xl mb-2">Please check your email</p>
<p>A sign in link has been sent to your email address.</p>
<div
<p>You can safely close this page.</p>
{/* <div
onClick={() =>
signIn("email", {
email,
email: email,
redirect: false,
})
}
className="mx-auto font-semibold mt-2 cursor-pointer w-fit"
>
Resend?
</div>
</div> */}
</div>
</div>
);

View File

@ -1,6 +1,6 @@
import EmailConfirmaion from "@/components/Modal/EmailConfirmaion";
import SubmitButton from "@/components/SubmitButton";
import { signIn } from "next-auth/react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { toast } from "react-hot-toast";
@ -11,7 +11,6 @@ interface FormData {
export default function Forgot() {
const [submitLoader, setSubmitLoader] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
const [form, setForm] = useState<FormData>({
email: "",
@ -23,20 +22,16 @@ export default function Forgot() {
const load = toast.loading("Sending login link...");
const res = await signIn("email", {
await signIn("email", {
email: form.email,
callbackUrl: window.location.origin,
callbackUrl: "/",
});
setShowConfirmation(true);
toast.dismiss(load);
setSubmitLoader(false);
if (!res?.ok) {
toast.error("Invalid login.");
}
toast.success("Login link sent.");
} else {
toast.error("Please fill out all the fields.");
}
@ -44,30 +39,35 @@ export default function Forgot() {
return (
<>
{showConfirmation && form.email ? (
<EmailConfirmaion email={form.email} />
) : undefined}
<p className="text-xl font-bold text-center text-sky-500 mt-10 mb-3">
Linkwarden
</p>
<div className="p-5 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
<div className="my-5 text-center">
<p className="text-3xl font-bold text-sky-500">Password reset</p>
<div className="p-5 mt-10 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
<div className="flex flex-col gap-2 sm:flex-row justify-between items-center mb-5">
<Image
src="/linkwarden.png"
width={1694}
height={483}
alt="Linkwarden"
className="h-12 w-fit"
/>
<div className="text-center sm:text-right">
<p className="text-3xl font-bold text-sky-500">Password Reset</p>
</div>
</div>
<p className="text-sm text-sky-500 w-fit font-semibold">Email</p>
<div>
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">Email</p>
<input
type="text"
placeholder="johnny@example.com"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
<p className="text-md text-gray-500">
Make sure to change your password in the profile settings afterwards.
<p className="text-md text-gray-500 mt-1">
Make sure to change your password in the profile settings
afterwards.
</p>
</div>
<SubmitButton
onClick={loginUser}
@ -75,12 +75,12 @@ export default function Forgot() {
className="mt-2 w-full text-center"
loading={submitLoader}
/>
</div>
<div className="flex items-baseline gap-1 justify-center my-3">
<div className="flex items-baseline gap-1 justify-center">
<Link href={"/login"} className="block text-sky-500 font-bold">
Go back
</Link>
</div>
</div>
</>
);
}

View File

@ -1,5 +1,6 @@
import SubmitButton from "@/components/SubmitButton";
import { signIn } from "next-auth/react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { toast } from "react-hot-toast";
@ -45,20 +46,27 @@ export default function Login() {
return (
<>
<p className="text-xl font-bold text-center text-sky-500 mt-10 mb-3">
Linkwarden
</p>
<div className="p-5 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
<div className="my-5 text-center">
<div className="p-5 my-10 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
<div className="text-right flex flex-col gap-2 sm:flex-row justify-between items-center mb-5">
<Image
src="/linkwarden.png"
width={1694}
height={483}
alt="Linkwarden"
className="h-12 w-fit"
/>
<div className="text-center sm:text-right">
<p className="text-3xl font-bold text-sky-500">Welcome back</p>
<p className="text-md font-semibold text-sky-400">
Sign in to your account
</p>
</div>
</div>
<p className="text-sm text-sky-500 w-fit font-semibold">
<div>
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
Username
{EmailProvider ? " or Email" : undefined}
{EmailProvider ? "/Email" : undefined}
</p>
<input
@ -66,39 +74,44 @@ export default function Login() {
placeholder="johnny"
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
</div>
<p className="text-sm text-sky-500 w-fit font-semibold">Password</p>
<div>
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
Password
</p>
<input
type="password"
placeholder="*****************"
placeholder="***********"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
{EmailProvider && (
<div className="w-fit ml-auto mt-1">
<Link href={"/forgot"} className="text-gray-500 font-semibold">
Forgot Password?
</Link>
</div>
)}
</div>
<SubmitButton
onClick={loginUser}
label="Login"
className="mt-2 w-full text-center"
loading={submitLoader}
/>
</div>
<div className="flex items-baseline gap-1 justify-center my-3">
<div className="flex items-baseline gap-1 justify-center">
<p className="w-fit text-gray-500">New here?</p>
<Link href={"/register"} className="block text-sky-500 font-bold">
Sign Up
</Link>
</div>
{EmailProvider && (
<div className="flex items-baseline gap-1 justify-center mb-3">
<p className="w-fit text-gray-500">Forgot your password?</p>
<Link href={"/forgot"} className="block text-sky-500 font-bold">
Send login link
</Link>
</div>
)}
</>
);
}

View File

@ -63,9 +63,9 @@ export default function PublicCollections() {
})}
</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-500">Linkwarden.</span>
</p>
</p> */}
</div>
) : (
<></>

View File

@ -3,7 +3,7 @@ import { useState } from "react";
import { toast } from "react-hot-toast";
import SubmitButton from "@/components/SubmitButton";
import { signIn } from "next-auth/react";
import EmailConfirmaion from "@/components/Modal/EmailConfirmaion";
import Image from "next/image";
const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
@ -17,7 +17,6 @@ type FormData = {
export default function Register() {
const [submitLoader, setSubmitLoader] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
const [form, setForm] = useState<FormData>({
name: "",
@ -50,7 +49,7 @@ export default function Register() {
const sendConfirmation = async () => {
await signIn("email", {
email: form.email,
redirect: false,
callbackUrl: "/",
});
};
@ -76,10 +75,7 @@ export default function Register() {
setSubmitLoader(false);
if (response.ok) {
if (form.email) {
await sendConfirmation();
setShowConfirmation(true);
}
if (form.email) await sendConfirmation();
toast.success(
EmailProvider
@ -99,90 +95,111 @@ export default function Register() {
return (
<>
{showConfirmation && form.email ? (
<EmailConfirmaion email={form.email} />
) : undefined}
<p className="text-xl font-bold text-center my-10 mb-3 text-sky-500">
Linkwarden
</p>
<div className="p-5 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
<div className="my-5 text-center">
<div className="p-5 mx-auto my-10 flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
<div className="flex flex-col gap-2 sm:flex-row justify-between items-center mb-5">
<Image
src="/linkwarden.png"
width={1694}
height={483}
alt="Linkwarden"
className="h-12 w-fit"
/>
<div className="text-center sm:text-right">
<p className="text-3xl font-bold text-sky-500">Get started</p>
<p className="text-md font-semibold text-sky-400">
Create a new account
</p>
</div>
</div>
<p className="text-sm text-sky-500 w-fit font-semibold">Display Name</p>
<div>
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
Display Name
</p>
<input
type="text"
placeholder="Johnny"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
</div>
<p className="text-sm text-sky-500 w-fit font-semibold">Username</p>
<div>
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
Username
</p>
<input
type="text"
placeholder="john"
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
</div>
{EmailProvider ? (
<>
<p className="text-sm text-sky-500 w-fit font-semibold">Email</p>
<div>
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
Email
</p>
<input
type="email"
placeholder="johnny@example.com"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
</>
</div>
) : undefined}
<p className="text-sm text-sky-500 w-fit font-semibold">Password</p>
<input
type="password"
placeholder="*****************"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
<p className="text-sm text-sky-500 w-fit font-semibold">
Re-enter Password
<div className="flex item-center gap-5">
<div>
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
Password
</p>
<input
type="password"
placeholder="*****************"
placeholder="***********"
value={form.password}
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-500 duration-100"
/>
</div>
<div>
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
Confirm Password
</p>
<input
type="password"
placeholder="***********"
value={form.passwordConfirmation}
onChange={(e) =>
setForm({ ...form, passwordConfirmation: e.target.value })
}
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
</div>
</div>
<SubmitButton
onClick={registerUser}
label="Sign Up"
className="mt-2 w-full text-center"
loading={submitLoader}
/>
</div>
<div className="flex items-baseline gap-1 justify-center my-3">
<div className="flex items-baseline gap-1 justify-center">
<p className="w-fit text-gray-500">Already have an account?</p>
<Link href={"/login"} className="block w-min text-sky-500 font-bold">
<Link href={"/login"} className="block text-sky-500 font-bold">
Login
</Link>
</div>
</div>
</>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

After

Width:  |  Height:  |  Size: 650 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: 191 KiB

After

Width:  |  Height:  |  Size: 190 KiB

BIN
public/linkwarden.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,19 +1 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}