Toasts (Notification Component) (#163)

* feature: responsive navbar

* style: nav links hover animatiosn

* style: better Input Fields

* refactor: use form submit instead of button onclick

* feature: loading states

* feature: log out confirmation

* feature: basic toast

* feature: Toast variants

* fix: use global toast provider

* feature: use toast instead of alert for auth routes

* fix(mobile): nav menu close on route change

* fix: field dark mode

* feature: redirect when login and logout

* refactor: group auth routes

* refactor: use @/app imports

* style: use Field on /upload

* fix: forward ref

* feature: Multi toast

* feature: add toasts to /upload

* refactor: new login in auth group

* chore: quote

* chore(pnpm): removed

* feature: toasty animations

* fix: build errors and warnings

* chore: remove irrelevant comments

* fix: use unique ids for toasts

---------

Co-authored-by: Stan Girard <girard.stanislas@gmail.com>
This commit is contained in:
!MAD! 2023-05-26 14:27:29 +05:30 committed by GitHub
parent e11183ed07
commit f69c64ead1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 531 additions and 4208 deletions

2
.gitignore vendored
View File

@ -50,4 +50,4 @@ streamlit-demo/.streamlit/secrets.toml
.frontend_env
backend/pandoc-*
**/yarn.lock
**/.pandoc-*
**/.pandoc-*

View File

@ -0,0 +1,83 @@
"use client";
import { useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { useSupabase } from "@/app/supabase-provider";
import Button from "@/app/components/ui/Button";
import Card from "@/app/components/ui/Card";
import Field from "@/app/components/ui/Field";
import PageHeading from "@/app/components/ui/PageHeading";
import Toast, { ToastRef } from "@/app/components/ui/Toast";
import Link from "next/link";
export default function Login() {
const { supabase } = useSupabase();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isPending, setIsPending] = useState(false);
const loginToast = useRef<ToastRef>(null);
const router = useRouter();
const handleLogin = async () => {
setIsPending(true);
const { data, error } = await supabase.auth.signInWithPassword({
email: email,
password: password,
});
if (error) {
console.error("Error logging in:", error.message);
loginToast.current?.publish({
variant: "danger",
text: error.message,
});
} else if (data) {
console.log("User logged in:", data);
loginToast.current?.publish({
variant: "success",
text: "Successfully logged in",
});
router.replace("/");
}
setIsPending(false);
};
return (
<main>
<section className="w-full min-h-screen h-full outline-none flex flex-col gap-5 items-center justify-center p-6">
<PageHeading title="Login" subtitle="Welcome back" />
<Card className="max-w-md w-full p-5 sm:p-10 text-left">
<form
onSubmit={(e) => {
e.preventDefault();
handleLogin();
}}
className="flex flex-col gap-2"
>
<Field
name="email"
required
type="email"
placeholder="Email"
onChange={(e) => setEmail(e.target.value)}
/>
<Field
name="password"
required
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<div className="flex flex-col items-center justify-center mt-2 gap-2">
<Button isLoading={isPending}>Login</Button>
<Link href="/signup">Don{"'"}t have an account? Sign up</Link>
</div>
</form>
</Card>
</section>
<Toast variant="success" ref={loginToast} />
</main>
);
}

View File

@ -0,0 +1,74 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { useSupabase } from "@/app/supabase-provider";
import Toast, { ToastRef } from "@/app/components/ui/Toast";
import PageHeading from "@/app/components/ui/PageHeading";
import Button from "@/app/components/ui/Button";
import Card from "@/app/components/ui/Card";
import Link from "next/link";
export default function Logout() {
const { supabase } = useSupabase();
const [isPending, setIsPending] = useState(false);
const logoutToast = useRef<ToastRef>(null);
const [error, setError] = useState("Unknown Error");
const router = useRouter();
const handleLogout = async () => {
setIsPending(true);
const { error } = await supabase.auth.signOut();
if (error) {
console.error("Error logging out:", error.message);
setError(error.message);
logoutToast.current?.publish({
variant: "danger",
text: `Error logging out: ${error.message}`,
});
} else {
console.log("User logged out");
logoutToast.current?.publish({
variant: "success",
text: "Logged out successfully",
});
router.replace("/");
}
setIsPending(false);
};
// useEffect(() => {
// handleLogout();
// }, []);
return (
<main>
<section className="w-full min-h-screen h-full outline-none flex flex-col gap-5 items-center justify-center p-6">
<PageHeading title="Logout" subtitle="See you next time" />
<Card className="max-w-md w-full p-5 sm:p-10 text-center flex flex-col items-center gap-5">
<h2 className="text-lg">Are you sure you want to sign out?</h2>
<div className="flex gap-5 items-center justify-center">
<Link href={"/"}>
<Button variant={"primary"}>Go back</Button>
</Link>
<Button
isLoading={isPending}
variant={"danger"}
onClick={() => handleLogout()}
>
Log Out
</Button>
</div>
</Card>
</section>
<Toast variant="success" ref={logoutToast}>
Logged Out Successfully
</Toast>
<Toast variant="danger" ref={logoutToast}>
{error}
</Toast>
</main>
);
}

View File

@ -0,0 +1,78 @@
"use client";
import Button from "@/app/components/ui/Button";
import Card from "@/app/components/ui/Card";
import Field from "@/app/components/ui/Field";
import PageHeading from "@/app/components/ui/PageHeading";
import Toast, { ToastRef } from "@/app/components/ui/Toast";
import { useSupabase } from "@/app/supabase-provider";
import { useRef, useState } from "react";
export default function SignUp() {
const { supabase } = useSupabase();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isPending, setIsPending] = useState(false);
const signupToast = useRef<ToastRef>(null);
const handleSignUp = async () => {
setIsPending(true);
const { data, error } = await supabase.auth.signUp({
email: email,
password: password,
});
if (error) {
console.error("Error signing up:", error.message);
signupToast.current?.publish({
variant: "danger",
text: `Error signing up: ${error.message}`,
});
} else if (data) {
console.log("User signed up");
signupToast.current?.publish({ variant: "success", text: "Sign" });
}
setIsPending(false);
};
return (
<main>
<section className="w-full min-h-screen h-full outline-none flex flex-col gap-5 items-center justify-center p-6">
<PageHeading title="Sign Up" subtitle="Create your account" />
<Card className="max-w-md w-full p-5 sm:p-10 text-left">
<form
onSubmit={(e) => {
e.preventDefault();
handleSignUp();
}}
className="flex flex-col gap-2"
>
<Field
name="email"
required
type="email"
placeholder="Email"
onChange={(e) => setEmail(e.target.value)}
/>
<Field
name="password"
required
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<div className="flex flex-col items-center justify-center mt-2 gap-2">
<Button isLoading={isPending}>Sign Up</Button>
</div>
</form>
</Card>
</section>
<Toast variant="success" ref={signupToast}>
<h1 className="font-bold">Confirmation Email sent</h1>
<p className="text-sm">Check your email.</p>
</Toast>
<Toast variant="danger" ref={signupToast} />
</main>
);
}

View File

@ -17,9 +17,9 @@ export default function ChatPage() {
const [temperature, setTemperature] = useState(0);
const [maxTokens, setMaxTokens] = useState(500);
const [isPending, setIsPending] = useState(false);
const { supabase, session } = useSupabase()
const { supabase, session } = useSupabase();
if (session === null) {
redirect('/login')
redirect("/login");
}
const askQuestion = async () => {
@ -36,7 +36,7 @@ export default function ChatPage() {
},
{
headers: {
'Authorization': `Bearer ${session.access_token}`,
Authorization: `Bearer ${session.access_token}`,
},
}
);
@ -48,7 +48,7 @@ export default function ChatPage() {
};
return (
<main className="min-h-screen w-full flex flex-col pt-20">
<main className="min-h-screen w-full flex flex-col pt-32">
<section className="flex flex-col justify-center items-center flex-1 gap-5 h-full">
<PageHeading
title="Chat with your brain"

View File

@ -10,7 +10,7 @@ interface MobileMenuProps {}
const MobileMenu: FC<MobileMenuProps> = ({}) => {
const [open, setOpen] = useState(false);
return (
<Dialog.Root onOpenChange={setOpen}>
<Dialog.Root onOpenChange={setOpen} open={open}>
<Dialog.Trigger asChild>
<button className="block sm:hidden" aria-label="open menu">
<MdMenu className="text-4xl" />
@ -29,7 +29,10 @@ const MobileMenu: FC<MobileMenuProps> = ({}) => {
>
<Dialog.Content asChild forceMount>
<div className="flex flex-col items-center justify-between py-24 flex-1 w-full bg-white dark:bg-black border border-black/10 dark:border-white/25 p-10 shadow-xl dark:shadow-primary/50 focus:outline-none cursor-auto z-50">
<NavItems className="text-3xl h-fit text-center flex-col items-center justify-center gap-10" />
<NavItems
setOpen={setOpen}
className="text-3xl h-fit text-center flex-col items-center justify-center gap-10"
/>
<p className="">
Get a Second Brain with{" "}

View File

@ -1,14 +1,15 @@
import { cn } from "@/lib/utils";
import Link from "next/link";
import { FC, HTMLAttributes, ReactNode } from "react";
import { Dispatch, FC, HTMLAttributes, ReactNode, SetStateAction } from "react";
import DarkModeToggle from "./DarkModeToggle";
import Button from "../ui/Button";
interface NavItemsProps extends HTMLAttributes<HTMLUListElement> {}
interface NavItemsProps extends HTMLAttributes<HTMLUListElement> {
setOpen?: Dispatch<SetStateAction<boolean>>;
}
const NavItems: FC<NavItemsProps> = ({ className, ...props }) => {
const NavItems: FC<NavItemsProps> = ({ className, setOpen, ...props }) => {
return (
// <div className={cn("flex flex-1 items-center", className)} {...props}>
<ul
className={cn(
"flex flex-row items-center gap-4 text-sm flex-1",
@ -18,14 +19,24 @@ const NavItems: FC<NavItemsProps> = ({ className, ...props }) => {
>
{process.env.NEXT_PUBLIC_ENV === "local" ? (
<>
<NavLink to="/upload">Upload</NavLink>
<NavLink to="/chat">Chat</NavLink>
<NavLink to="/explore">Explore</NavLink>
<NavLink setOpen={setOpen} to="/upload">
Upload
</NavLink>
<NavLink setOpen={setOpen} to="/chat">
Chat
</NavLink>
<NavLink setOpen={setOpen} to="/explore">
Explore
</NavLink>
</>
) : (
<>
<NavLink to="https://github.com/StanGirard/quivr">Github</NavLink>
<NavLink to="https://discord.gg/HUpRgp2HG8">Discord</NavLink>
<NavLink setOpen={setOpen} to="https://github.com/StanGirard/quivr">
Github
</NavLink>
<NavLink setOpen={setOpen} to="https://discord.gg/HUpRgp2HG8">
Discord
</NavLink>
</>
)}
<div className="flex sm:flex-1 sm:justify-end flex-col items-center justify-center sm:flex-row gap-5 sm:gap-2">
@ -35,19 +46,21 @@ const NavItems: FC<NavItemsProps> = ({ className, ...props }) => {
<DarkModeToggle />
</div>
</ul>
// </div>
);
};
interface NavLinkProps {
children: ReactNode;
to: string;
setOpen?: Dispatch<SetStateAction<boolean>>;
}
const NavLink: FC<NavLinkProps> = ({ children, to }) => {
const NavLink: FC<NavLinkProps> = ({ children, to, setOpen }) => {
return (
<li className="group relative">
<Link href={to}>{children}</Link>
<Link onClick={() => setOpen && setOpen(false)} href={to}>
{children}
</Link>
<hr className="aboslute top-full border border-transparent border-b-primary dark:border-b-white scale-x-0 group-hover:scale-x-100 group-focus-within:scale-x-100 transition-transform" />
</li>
);

View File

@ -28,7 +28,7 @@ const NavBar: FC<NavBarProps> = ({}) => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
}, [hidden]);
return (
<motion.header

View File

@ -15,7 +15,7 @@ const ButtonVariants = cva(
variants: {
variant: {
primary:
"bg-black disabled:bg-gray-500 disabled:hover:bg-gray-500 text-white dark:bg-white dark:text-black hover:bg-gray-700 dark:hover:bg-gray-200 transition-colors",
"bg-black border border-black dark:border-white disabled:bg-gray-500 disabled:hover:bg-gray-500 text-white dark:bg-white dark:text-black hover:bg-gray-700 dark:hover:bg-gray-200 transition-colors",
tertiary: "text-black dark:text-white bg-transparent py-2 px-4",
secondary:
"border border-black dark:border-white bg-white dark:bg-black text-black dark:text-white focus:bg-black dark:focus:bg-white hover:bg-black dark:hover:bg-white hover:text-white dark:hover:text-black focus:text-white dark:focus:text-black transition-colors py-2 px-4 shadow-none",

View File

@ -0,0 +1,41 @@
import { cn } from "@/lib/utils";
import {
DetailedHTMLProps,
FC,
InputHTMLAttributes,
RefObject,
forwardRef,
} from "react";
interface FieldProps
extends DetailedHTMLProps<
InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
label?: string;
name: string;
}
const Field = forwardRef(
({ label, className, name, id, ...props }: FieldProps, forwardedRef) => {
return (
<fieldset className={cn("flex flex-col w-full", className)} name={name}>
{label && (
<label htmlFor={name} className="text-sm">
{label}
</label>
)}
<input
ref={forwardedRef as RefObject<HTMLInputElement>}
className="w-full bg-gray-50 dark:bg-gray-900 px-4 py-2 border rounded-md border-black/10 dark:border-white/25"
name={name}
id={name}
{...props}
/>
</fieldset>
);
}
);
Field.displayName = "Field";
export default Field;

View File

@ -7,7 +7,7 @@ interface PageHeadingProps {
const PageHeading: FC<PageHeadingProps> = ({ title, subtitle }) => {
return (
<div className="flex flex-col items-center justify-center mt-10">
<div className="flex flex-col items-center justify-center">
<h1 className="text-3xl font-bold text-center">{title}</h1>
{subtitle && <h2 className="opacity-50">{subtitle}</h2>}
</div>

View File

@ -0,0 +1,118 @@
"use client";
import { ReactNode, forwardRef, useImperativeHandle, useState } from "react";
import * as ToastPrimitive from "@radix-ui/react-toast";
import Button from "./Button";
import { VariantProps, cva } from "class-variance-authority";
import { cn, generateUniqueId } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
export interface ToastRef {
publish: (toast: ToastContent) => void;
}
const ToastVariants = cva(
"bg-white dark:bg-black px-8 max-w-sm w-full py-5 border border-black/10 dark:border-white/25 rounded-xl shadow-xl flex items-center pointer-events-auto data-[swipe=end]:opacity-0 data-[state=closed]:opacity-0 transition-opacity",
{
variants: {
variant: {
neutral: "",
danger: "bg-red-400 dark:bg-red-600",
success: "bg-green-400 dark:bg-green-600",
},
},
defaultVariants: {
variant: "neutral",
},
}
);
interface ToastContent extends VariantProps<typeof ToastVariants> {
text: string;
open?: boolean;
id?: string;
}
interface ToastProps
extends ToastPrimitive.ToastProps,
VariantProps<typeof ToastVariants> {
children?: ReactNode;
}
export const Toast = forwardRef(
({ children, variant, ...props }: ToastProps, forwardedRef) => {
const [toasts, setToasts] = useState<ToastContent[]>([]);
const toggleToast = (value: boolean, index: number) => {
setToasts((toasts) =>
toasts.map((toast, i) => {
if (i === index) {
toast.open = value;
}
return toast;
})
);
};
useImperativeHandle(
forwardedRef,
(): ToastRef => ({
publish: (toast: ToastContent) => {
setToasts((toasts) => {
const newToasts = [...toasts];
toast.open = true;
toast.id = generateUniqueId();
newToasts.push(toast);
return newToasts;
});
},
})
);
return (
<>
<AnimatePresence mode="popLayout">
{toasts.map((toast, index) => {
if (!toast.open) return;
return (
<ToastPrimitive.Root
open={toast.open}
onOpenChange={(value) => toggleToast(value, index)}
asChild
forceMount
key={toast.id}
{...props}
>
<motion.div
layout
initial={{ x: "100%", opacity: 0 }}
animate={{
x: "0%",
opacity: 1,
}}
exit={{ opacity: 0 }}
className={cn(ToastVariants({ variant: toast.variant }))}
>
<ToastPrimitive.Description className="flex-1">
{toast.text}
</ToastPrimitive.Description>
<ToastPrimitive.Close asChild>
<Button variant={"tertiary"}>Dismiss</Button>
</ToastPrimitive.Close>
</motion.div>
</ToastPrimitive.Root>
);
})}
</AnimatePresence>
<ToastPrimitive.Viewport className="fixed flex-col bottom-0 left-0 right-0 p-5 flex items-end gap-2 outline-none pointer-events-none" />
</>
);
}
);
export const ToastProvider = ({ children }: { children?: ReactNode }) => {
return <ToastPrimitive.Provider>{children}</ToastPrimitive.Provider>;
};
Toast.displayName = "Toast";
export default Toast;

View File

@ -10,20 +10,14 @@ import { AnimatePresence } from "framer-motion";
import { useSupabase } from "../supabase-provider";
import { redirect } from "next/navigation";
export default function ExplorePage() {
const [documents, setDocuments] = useState<Document[]>([]);
const [isPending, setIsPending] = useState(true);
const { supabase, session } = useSupabase();
if (session === null) {
redirect('/login')
redirect("/login");
}
useEffect(() => {
fetchDocuments();
}, []);
const fetchDocuments = async () => {
setIsPending(true);
try {
@ -34,7 +28,7 @@ export default function ExplorePage() {
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore`,
{
headers: {
'Authorization': `Bearer ${session.access_token}`,
Authorization: `Bearer ${session.access_token}`,
},
}
);
@ -46,12 +40,20 @@ export default function ExplorePage() {
setIsPending(false);
};
useEffect(() => {
fetchDocuments();
}, [fetchDocuments]);
return (
<main>
<section className="w-full outline-none pt-20 flex flex-col gap-5 items-center justify-center p-6">
<section className="w-full outline-none pt-32 flex flex-col gap-5 items-center justify-center p-6">
<div className="flex flex-col items-center justify-center my-10">
<h1 className="text-3xl font-bold text-center">Explore uploaded data</h1>
<h2 className="opacity-50">View or delete stored data used by your brain</h2>
<h1 className="text-3xl font-bold text-center">
Explore uploaded data
</h1>
<h2 className="opacity-50">
View or delete stored data used by your brain
</h2>
</div>
{isPending ? (
<Spinner />

View File

@ -2,9 +2,10 @@ import { Analytics } from "@vercel/analytics/react";
import NavBar from "./components/NavBar";
import "./globals.css";
import { Inter } from "next/font/google";
import SupabaseProvider from './supabase-provider'
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { headers, cookies } from 'next/headers'
import SupabaseProvider from "./supabase-provider";
import { createServerComponentSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { headers, cookies } from "next/headers";
import { ToastProvider } from "./components/ui/Toast";
const inter = Inter({ subsets: ["latin"] });
@ -14,16 +15,19 @@ export const metadata = {
"Quivr is your second brain in the cloud, designed to easily store and retrieve unstructured information.",
};
export default async function RootLayout({ children }: { children: React.ReactNode }) {
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const supabase = createServerComponentSupabaseClient({
headers,
cookies,
})
});
const {
data: { session },
} = await supabase.auth.getSession()
} = await supabase.auth.getSession();
return (
<html lang="en">
@ -31,10 +35,11 @@ export default async function RootLayout({ children }: { children: React.ReactNo
className={`bg-white text-black dark:bg-black dark:text-white min-h-screen w-full ${inter.className}`}
>
<NavBar />
<SupabaseProvider session={session}>{children}</SupabaseProvider>
<ToastProvider>
<SupabaseProvider session={session}>{children}</SupabaseProvider>
</ToastProvider>
<Analytics />
</body>
</html>
)
);
}

View File

@ -1,60 +0,0 @@
"use client";
import { useState } from "react";
import Card from "../components/ui/Card";
import Button from "../components/ui/Button";
import PageHeading from "../components/ui/PageHeading";
import { useSupabase } from "../supabase-provider";
import { redirect } from "next/navigation";
import Link from "next/link";
export default function Login() {
const { supabase } = useSupabase();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
const { data, error } = await supabase.auth.signInWithPassword({
email: email,
password: password,
});
if (error) {
console.error("Error logging in:", error.message);
alert(`Error logging in: ${error.message}`);
} else if (data) {
console.log("User logged in:", data);
alert("Login successful!");
}
};
return (
<main>
<section className="w-full outline-none pt-20 flex flex-col gap-5 items-center justify-center p-6">
<PageHeading title="Login" subtitle="Welcome back" />
<Card className="w-1/2 flex justify-center items-center">
<div className="text-center mt-2 p-6 max-w-sm w-full flex flex-col gap-5 items-center">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
className="w-full py-2 px-4 rounded-md border border-gray-300 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="w-full py-2 px-4 rounded-md border border-gray-300 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent mt-2"
/>
<div className="grid place-items-center gap-3">
<Button onClick={handleLogin}>Login</Button>
<Link href="/signup">{"Don't have an account? Sign up"}</Link>
</div>
</div>
</Card>
</section>
</main>
);
}

View File

@ -1,38 +0,0 @@
"use client";
import { useEffect } from "react";
import Card from "../components/ui/Card";
import PageHeading from "../components/ui/PageHeading";
import { useSupabase } from "../supabase-provider";
export default function Logout() {
const { supabase } = useSupabase();
const handleLogout = async () => {
const { error } = await supabase.auth.signOut();
if (error) {
console.error("Error logging out:", error.message);
alert(`Error logging out: ${error.message}`);
} else {
console.log("User logged out");
alert("Logout successful!");
}
};
useEffect(() => {
handleLogout();
}, []);
return (
<main>
<section className="w-full outline-none pt-20 flex flex-col gap-5 items-center justify-center p-6">
<PageHeading title="Logout" subtitle="See you next time"/>
<Card className="w-1/2 flex justify-center items-center">
<div className="text-center mt-2 p-6 max-w-sm w-full flex flex-col gap-5 items-center">
<h1>You are now logged out.</h1>
</div>
</Card>
</section>
</main>
);
}

View File

@ -1,58 +0,0 @@
"use client";
import { useState } from "react";
import Card from "../components/ui/Card";
import Button from "../components/ui/Button";
import PageHeading from "../components/ui/PageHeading";
import { useSupabase } from "../supabase-provider";
export default function SignUp() {
const { supabase } = useSupabase();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSignUp = async () => {
const { data, error } = await supabase.auth.signUp({
email: email,
password: password,
});
if (error) {
console.error("Error signing up:", error.message);
alert(`Error signing up: ${error.message}`);
} else if (data) {
console.log("User signed up:", data);
alert("Signup successful!");
}
};
return (
<main
>
<section className="w-full outline-none pt-20 flex flex-col gap-5 items-center justify-center p-6">
<PageHeading title="SignUp" subtitle="Create your account"/>
<Card className="w-1/2 flex justify-center items-center">
<div className="text-center mt-2 p-6 max-w-sm w-full flex flex-col gap-5 items-center">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
className="text-center"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="text-center"
/>
<div className="flex justify-center gap-3">
<Button onClick={handleSignUp}>Sign Up</Button>
</div>
</div>
</Card>
</section>
</main>
);
}

View File

@ -1,5 +1,12 @@
"use client";
import { Dispatch, SetStateAction, useCallback, useState, useRef } from "react";
import {
Dispatch,
SetStateAction,
useCallback,
useState,
useRef,
useEffect,
} from "react";
import { FileRejection, useDropzone } from "react-dropzone";
import axios from "axios";
import { Message } from "@/lib/types";
@ -11,6 +18,8 @@ import Card from "../components/ui/Card";
import PageHeading from "../components/ui/PageHeading";
import { useSupabase } from "../supabase-provider";
import { redirect } from "next/navigation";
import Field from "../components/ui/Field";
import Toast, { ToastRef } from "../components/ui/Toast";
export default function UploadPage() {
const [message, setMessage] = useState<Message | null>(null);
@ -18,11 +27,26 @@ export default function UploadPage() {
const [files, setFiles] = useState<File[]>([]);
const [pendingFileIndex, setPendingFileIndex] = useState<number>(0);
const urlInputRef = useRef<HTMLInputElement | null>(null);
const { supabase, session } = useSupabase()
const { supabase, session } = useSupabase();
if (session === null) {
redirect('/login')
redirect("/login");
}
const messageToast = useRef<ToastRef>(null);
useEffect(() => {
if (!message) return;
messageToast.current?.publish({
variant:
message.type === "error"
? "danger"
: message.type === "warning"
? "neutral"
: "success",
text: message.text,
});
}, [message]);
const crawlWebsite = useCallback(async () => {
// Validate URL
const url = urlInputRef.current ? urlInputRef.current.value : null;
@ -50,7 +74,7 @@ export default function UploadPage() {
config,
{
headers: {
'Authorization': `Bearer ${session.access_token}`,
Authorization: `Bearer ${session.access_token}`,
},
}
);
@ -65,36 +89,39 @@ export default function UploadPage() {
text: "Failed to crawl website: " + error.toString(),
});
}
}, []);
}, [session.access_token]);
const upload = useCallback(async (file: File) => {
const formData = new FormData();
formData.append("file", file);
try {
const response = await axios.post(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/upload`,
formData,
{
headers: {
'Authorization': `Bearer ${session.access_token}`,
},
}
);
const upload = useCallback(
async (file: File) => {
const formData = new FormData();
formData.append("file", file);
try {
const response = await axios.post(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/upload`,
formData,
{
headers: {
Authorization: `Bearer ${session.access_token}`,
},
}
);
setMessage({
type: response.data.type,
text:
(response.data.type === "success"
? "File uploaded successfully: "
: "") + JSON.stringify(response.data.message),
});
} catch (error: any) {
setMessage({
type: "error",
text: "Failed to upload file: " + error.toString(),
});
}
}, []);
setMessage({
type: response.data.type,
text:
(response.data.type === "success"
? "File uploaded successfully: "
: "") + JSON.stringify(response.data.message),
});
} catch (error: any) {
setMessage({
type: "error",
text: "Failed to upload file: " + error.toString(),
});
}
},
[session.access_token]
);
const onDrop = (acceptedFiles: File[], fileRejections: FileRejection[]) => {
if (fileRejections.length > 0) {
@ -138,7 +165,7 @@ export default function UploadPage() {
<section
{...getRootProps()}
// className="w-full h-full min-h-screen text-center flex flex-col items-center gap-5 pt-32 outline-none"
className="w-full outline-none pt-20 flex flex-col gap-5 items-center justify-center p-6"
className="w-full outline-none pt-32 flex flex-col gap-5 items-center justify-center p-6"
>
<PageHeading
title="Upload Knowledge"
@ -177,11 +204,11 @@ export default function UploadPage() {
{/* Assign a width of 50% to each card */}
<Card className="w-1/2">
<div className="text-center mt-2 p-6 max-w-sm w-full flex flex-col gap-5 items-center">
<input
<Field
name="crawlurl"
ref={urlInputRef}
type="text"
placeholder="Enter a website URL"
className="dark:bg-black"
/>
<button
onClick={crawlWebsite}
@ -203,19 +230,7 @@ export default function UploadPage() {
</Link>
</div>
</section>
{message && (
<div
className={`fixed bottom-0 inset-x-0 m-4 p-4 max-w-sm mx-auto rounded ${
message.type === "success"
? "bg-green-500"
: message.type === "warning"
? "bg-yellow-600"
: "bg-red-500"
}`}
>
<p className="text-white">{message.text}</p>
</div>
)}
<Toast ref={messageToast} />
</main>
);
}

View File

@ -2,5 +2,11 @@ import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs));
}
export function generateUniqueId() {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 10000);
return `${timestamp}-${random}`;
}

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-toast": "^1.1.3",
"@supabase/auth-helpers-nextjs": "^0.6.1",
"@supabase/auth-ui-react": "^0.4.2",
"@supabase/auth-ui-shared": "^0.1.6",

File diff suppressed because it is too large Load Diff