mirror of
https://github.com/StanGirard/quivr.git
synced 2024-12-25 20:32:11 +03:00
feat(toast): add global publisher (#177)
This commit is contained in:
parent
d44e9e1984
commit
e388990384
@ -1,11 +1,11 @@
|
|||||||
import { useToast } from "@/app/hooks/useToast";
|
|
||||||
import { useSupabase } from "@/app/supabase-provider";
|
import { useSupabase } from "@/app/supabase-provider";
|
||||||
|
import { useToast } from "@/lib/hooks/useToast";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const useGoogleLogin = () => {
|
export const useGoogleLogin = () => {
|
||||||
const { supabase } = useSupabase();
|
const { supabase } = useSupabase();
|
||||||
|
|
||||||
const { setMessage, messageToast } = useToast();
|
const { publish } = useToast();
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const signInWithGoogle = async () => {
|
const signInWithGoogle = async () => {
|
||||||
const { error } = await supabase.auth.signInWithOAuth({
|
const { error } = await supabase.auth.signInWithOAuth({
|
||||||
@ -19,8 +19,8 @@ export const useGoogleLogin = () => {
|
|||||||
});
|
});
|
||||||
setIsPending(false);
|
setIsPending(false);
|
||||||
if (error) {
|
if (error) {
|
||||||
setMessage({
|
publish({
|
||||||
type: "error",
|
variant: "danger",
|
||||||
text: "An error occurred ",
|
text: "An error occurred ",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -29,6 +29,5 @@ export const useGoogleLogin = () => {
|
|||||||
return {
|
return {
|
||||||
signInWithGoogle,
|
signInWithGoogle,
|
||||||
isPending,
|
isPending,
|
||||||
messageToast,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,21 +1,18 @@
|
|||||||
import Button from "@/app/components/ui/Button";
|
import Button from "@/app/components/ui/Button";
|
||||||
import Toast from "@/app/components/ui/Toast";
|
|
||||||
import { useGoogleLogin } from "./hooks/useGoogleLogin";
|
import { useGoogleLogin } from "./hooks/useGoogleLogin";
|
||||||
|
|
||||||
export const GoogleLoginButton = () => {
|
export const GoogleLoginButton = () => {
|
||||||
const { isPending, messageToast, signInWithGoogle } = useGoogleLogin();
|
const { isPending, signInWithGoogle } = useGoogleLogin();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Button
|
||||||
<Button
|
onClick={signInWithGoogle}
|
||||||
onClick={signInWithGoogle}
|
isLoading={isPending}
|
||||||
isLoading={isPending}
|
variant={"secondary"}
|
||||||
variant={"secondary"}
|
type="button"
|
||||||
type="button"
|
>
|
||||||
>
|
Login with Google
|
||||||
Login with Google
|
</Button>
|
||||||
</Button>
|
|
||||||
<Toast ref={messageToast} />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Button from "@/app/components/ui/Button";
|
import Button from "@/app/components/ui/Button";
|
||||||
import Toast from "@/app/components/ui/Toast";
|
|
||||||
import { useToast } from "@/app/hooks/useToast";
|
|
||||||
import { useSupabase } from "@/app/supabase-provider";
|
import { useSupabase } from "@/app/supabase-provider";
|
||||||
|
import { useToast } from "@/lib/hooks/useToast";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
type MaginLinkLoginProps = {
|
type MaginLinkLoginProps = {
|
||||||
@ -14,12 +13,12 @@ export const MagicLinkLogin = ({ email, setEmail }: MaginLinkLoginProps) => {
|
|||||||
const { supabase } = useSupabase();
|
const { supabase } = useSupabase();
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
const { setMessage, messageToast } = useToast();
|
const { publish } = useToast();
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (email === "") {
|
if (email === "") {
|
||||||
setMessage({
|
publish({
|
||||||
type: "error",
|
variant: "danger",
|
||||||
text: "Please enter your email address",
|
text: "Please enter your email address",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -35,13 +34,13 @@ export const MagicLinkLogin = ({ email, setEmail }: MaginLinkLoginProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
setMessage({
|
publish({
|
||||||
type: "danger",
|
variant: "danger",
|
||||||
text: error.message,
|
text: error.message,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setMessage({
|
publish({
|
||||||
type: "success",
|
variant: "success",
|
||||||
text: "Magic link sent successfully if email recognized",
|
text: "Magic link sent successfully if email recognized",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,16 +50,13 @@ export const MagicLinkLogin = ({ email, setEmail }: MaginLinkLoginProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Button
|
||||||
<Button
|
type="button"
|
||||||
type="button"
|
variant={"tertiary"}
|
||||||
variant={"tertiary"}
|
onClick={handleLogin}
|
||||||
onClick={handleLogin}
|
isLoading={isPending}
|
||||||
isLoading={isPending}
|
>
|
||||||
>
|
Send Magic Link
|
||||||
Send Magic Link
|
</Button>
|
||||||
</Button>
|
|
||||||
<Toast ref={messageToast} />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,9 +4,8 @@ import Card from "@/app/components/ui/Card";
|
|||||||
import { Divider } from "@/app/components/ui/Divider";
|
import { Divider } from "@/app/components/ui/Divider";
|
||||||
import Field from "@/app/components/ui/Field";
|
import Field from "@/app/components/ui/Field";
|
||||||
import PageHeading from "@/app/components/ui/PageHeading";
|
import PageHeading from "@/app/components/ui/PageHeading";
|
||||||
import Toast from "@/app/components/ui/Toast";
|
|
||||||
import { useToast } from "@/app/hooks/useToast";
|
|
||||||
import { useSupabase } from "@/app/supabase-provider";
|
import { useSupabase } from "@/app/supabase-provider";
|
||||||
|
import { useToast } from "@/lib/hooks/useToast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@ -19,7 +18,7 @@ export default function Login() {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
const { setMessage, messageToast } = useToast();
|
const { publish } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -31,17 +30,15 @@ export default function Login() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
setMessage({
|
publish({
|
||||||
type: "danger",
|
variant: "danger",
|
||||||
text: error.message,
|
text: error.message,
|
||||||
});
|
});
|
||||||
} else if (data) {
|
} else if (data) {
|
||||||
setMessage({
|
publish({
|
||||||
type: "success",
|
variant: "success",
|
||||||
text: "Successfully logged in",
|
text: "Successfully logged in",
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
setEmail("");
|
|
||||||
}
|
}
|
||||||
setIsPending(false);
|
setIsPending(false);
|
||||||
};
|
};
|
||||||
@ -68,6 +65,7 @@ export default function Login() {
|
|||||||
type="email"
|
type="email"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
value={email}
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
name="password"
|
name="password"
|
||||||
@ -92,7 +90,6 @@ export default function Login() {
|
|||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
<Toast ref={messageToast} />
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,17 @@
|
|||||||
import Button from "@/app/components/ui/Button";
|
import Button from "@/app/components/ui/Button";
|
||||||
import Card from "@/app/components/ui/Card";
|
import Card from "@/app/components/ui/Card";
|
||||||
import PageHeading from "@/app/components/ui/PageHeading";
|
import PageHeading from "@/app/components/ui/PageHeading";
|
||||||
import Toast, { ToastRef } from "@/app/components/ui/Toast";
|
|
||||||
import { useSupabase } from "@/app/supabase-provider";
|
import { useSupabase } from "@/app/supabase-provider";
|
||||||
|
import { useToast } from "@/lib/hooks/useToast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useRef, useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function Logout() {
|
export default function Logout() {
|
||||||
const { supabase } = useSupabase();
|
const { supabase } = useSupabase();
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
const logoutToast = useRef<ToastRef>(null);
|
const { publish } = useToast();
|
||||||
const [error, setError] = useState("Unknown Error");
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
@ -23,14 +21,12 @@ export default function Logout() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("Error logging out:", error.message);
|
console.error("Error logging out:", error.message);
|
||||||
setError(error.message);
|
publish({
|
||||||
logoutToast.current?.publish({
|
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
text: `Error logging out: ${error.message}`,
|
text: `Error logging out: ${error.message}`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("User logged out");
|
publish({
|
||||||
logoutToast.current?.publish({
|
|
||||||
variant: "success",
|
variant: "success",
|
||||||
text: "Logged out successfully",
|
text: "Logged out successfully",
|
||||||
});
|
});
|
||||||
@ -39,10 +35,6 @@ export default function Logout() {
|
|||||||
setIsPending(false);
|
setIsPending(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// handleLogout();
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<section className="w-full min-h-screen h-full outline-none flex flex-col gap-5 items-center justify-center p-6">
|
<section className="w-full min-h-screen h-full outline-none flex flex-col gap-5 items-center justify-center p-6">
|
||||||
@ -63,12 +55,6 @@ export default function Logout() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
<Toast variant="success" ref={logoutToast}>
|
|
||||||
Logged Out Successfully
|
|
||||||
</Toast>
|
|
||||||
<Toast variant="danger" ref={logoutToast}>
|
|
||||||
{error}
|
|
||||||
</Toast>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@ import Button from "@/app/components/ui/Button";
|
|||||||
import Card from "@/app/components/ui/Card";
|
import Card from "@/app/components/ui/Card";
|
||||||
import Field from "@/app/components/ui/Field";
|
import Field from "@/app/components/ui/Field";
|
||||||
import PageHeading from "@/app/components/ui/PageHeading";
|
import PageHeading from "@/app/components/ui/PageHeading";
|
||||||
import Toast, { ToastRef } from "@/app/components/ui/Toast";
|
|
||||||
import { useSupabase } from "@/app/supabase-provider";
|
import { useSupabase } from "@/app/supabase-provider";
|
||||||
|
import { useToast } from "@/lib/hooks/useToast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRef, useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function SignUp() {
|
export default function SignUp() {
|
||||||
const { supabase } = useSupabase();
|
const { supabase } = useSupabase();
|
||||||
@ -14,8 +14,7 @@ export default function SignUp() {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
const signupToast = useRef<ToastRef>(null);
|
const { publish } = useToast();
|
||||||
|
|
||||||
const handleSignUp = async () => {
|
const handleSignUp = async () => {
|
||||||
setIsPending(true);
|
setIsPending(true);
|
||||||
const { data, error } = await supabase.auth.signUp({
|
const { data, error } = await supabase.auth.signUp({
|
||||||
@ -25,13 +24,15 @@ export default function SignUp() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("Error signing up:", error.message);
|
console.error("Error signing up:", error.message);
|
||||||
signupToast.current?.publish({
|
publish({
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
text: `Error signing up: ${error.message}`,
|
text: `Error signing up: ${error.message}`,
|
||||||
});
|
});
|
||||||
} else if (data) {
|
} else if (data) {
|
||||||
console.log("User signed up");
|
publish({
|
||||||
signupToast.current?.publish({ variant: "success", text: "Sign" });
|
variant: "success",
|
||||||
|
text: "Confirmation Email sent, please check your email",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setIsPending(false);
|
setIsPending(false);
|
||||||
};
|
};
|
||||||
@ -70,11 +71,6 @@ export default function SignUp() {
|
|||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</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>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -41,8 +41,6 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
setHistory(response.data.history);
|
setHistory(response.data.history);
|
||||||
console.log(response.data.history);
|
|
||||||
|
|
||||||
setQuestion("");
|
setQuestion("");
|
||||||
setIsPending(false);
|
setIsPending(false);
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,9 @@ const Divider: FC<DividerProps> = forwardRef(
|
|||||||
>
|
>
|
||||||
<hr className="border-t border-gray-300 w-12" />
|
<hr className="border-t border-gray-300 w-12" />
|
||||||
{text !== undefined && (
|
{text !== undefined && (
|
||||||
<p className="px-3 bg-white text-center text-gray-500">{text}</p>
|
<p className="px-3 text-center text-gray-500 dark:text-white">
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
<hr className="border-t border-gray-300 w-12" />
|
<hr className="border-t border-gray-300 w-12" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { cn, generateUniqueId } from "@/lib/utils";
|
|
||||||
import * as ToastPrimitive from "@radix-ui/react-toast";
|
|
||||||
import { VariantProps, cva } from "class-variance-authority";
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
|
||||||
import { ReactNode, forwardRef, useImperativeHandle, useState } from "react";
|
|
||||||
import Button from "./Button";
|
|
||||||
|
|
||||||
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(({ ...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;
|
|
60
frontend/app/components/ui/Toast/components/Toast.tsx
Normal file
60
frontend/app/components/ui/Toast/components/Toast.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"use client";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import * as ToastPrimitive from "@radix-ui/react-toast";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import Button from "../../Button";
|
||||||
|
import { ToastContext } from "../domain/ToastContext";
|
||||||
|
import { ToastVariants } from "../domain/types";
|
||||||
|
import { useToastBuilder } from "../hooks/useToastBuilder";
|
||||||
|
|
||||||
|
export const Toast = ({
|
||||||
|
children,
|
||||||
|
...toastProviderProps
|
||||||
|
}: { children?: ReactNode } & ToastPrimitive.ToastProviderProps) => {
|
||||||
|
const { publish, toasts, toggleToast } = useToastBuilder();
|
||||||
|
return (
|
||||||
|
<ToastPrimitive.Provider {...toastProviderProps}>
|
||||||
|
<ToastContext.Provider value={{ publish }}>
|
||||||
|
{children}
|
||||||
|
<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}
|
||||||
|
>
|
||||||
|
<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"} className="text-white">
|
||||||
|
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" />
|
||||||
|
</ToastContext.Provider>
|
||||||
|
</ToastPrimitive.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Toast.displayName = "Toast";
|
11
frontend/app/components/ui/Toast/components/index.tsx
Normal file
11
frontend/app/components/ui/Toast/components/index.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
import * as ToastPrimitive from "@radix-ui/react-toast";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { Toast } from "./Toast";
|
||||||
|
|
||||||
|
export const ToastProvider = ({
|
||||||
|
children,
|
||||||
|
...toastProviderProps
|
||||||
|
}: { children?: ReactNode } & ToastPrimitive.ToastProviderProps) => {
|
||||||
|
return <Toast {...toastProviderProps}>{children}</Toast>;
|
||||||
|
};
|
6
frontend/app/components/ui/Toast/domain/ToastContext.ts
Normal file
6
frontend/app/components/ui/Toast/domain/ToastContext.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
import { ToastPublisher } from "./types";
|
||||||
|
|
||||||
|
const publish: ToastPublisher = () => void 0;
|
||||||
|
|
||||||
|
export const ToastContext = createContext({ publish });
|
32
frontend/app/components/ui/Toast/domain/types.ts
Normal file
32
frontend/app/components/ui/Toast/domain/types.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { VariantProps, cva } from "class-variance-authority";
|
||||||
|
|
||||||
|
export 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: "bg-gray-500 dark:bg-gray-600",
|
||||||
|
danger: "bg-red-500 text-white dark:bg-red-600",
|
||||||
|
success: "bg-green-500 text-white dark:bg-green-600",
|
||||||
|
warning: "bg-orange-500 text-white dark:bg-orange-600",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "neutral",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type ToastVariant = NonNullable<VariantProps<typeof ToastVariants>["variant"]>;
|
||||||
|
|
||||||
|
export interface ToastData {
|
||||||
|
text: string;
|
||||||
|
variant: ToastVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToastContent extends ToastData {
|
||||||
|
open?: boolean;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ToastPublisher = (toast: ToastData) => void;
|
@ -0,0 +1,5 @@
|
|||||||
|
export const generateToastUniqueId = () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.floor(Math.random() * 10000);
|
||||||
|
return `${timestamp}-${random}`;
|
||||||
|
};
|
32
frontend/app/components/ui/Toast/hooks/useToastBuilder.ts
Normal file
32
frontend/app/components/ui/Toast/hooks/useToastBuilder.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { ToastContent, ToastData, ToastPublisher } from "../domain/types";
|
||||||
|
import { generateToastUniqueId } from "../helpers/generateToastUniqueId";
|
||||||
|
|
||||||
|
// ⚠️ You should not probably import this. use `useToast` instead
|
||||||
|
export const useToastBuilder = () => {
|
||||||
|
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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const publish: ToastPublisher = (newTost: ToastData) => {
|
||||||
|
setToasts((toasts) => [
|
||||||
|
...toasts,
|
||||||
|
{ ...newTost, open: true, id: generateToastUniqueId() },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
publish,
|
||||||
|
toggleToast,
|
||||||
|
toasts,
|
||||||
|
};
|
||||||
|
};
|
1
frontend/app/components/ui/Toast/index.ts
Normal file
1
frontend/app/components/ui/Toast/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./components/index";
|
@ -24,7 +24,6 @@ const DocumentItem = ({ document, setDocuments }: DocumentProps) => {
|
|||||||
const deleteDocument = async (name: string) => {
|
const deleteDocument = async (name: string) => {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
try {
|
try {
|
||||||
console.log(`Deleting Document ${name}`);
|
|
||||||
await axios.delete(
|
await axios.delete(
|
||||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${name}`,
|
`${process.env.NEXT_PUBLIC_BACKEND_URL}/explore/${name}`,
|
||||||
{
|
{
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
import { Message } from "postcss";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { ToastRef } from "../components/ui/Toast";
|
|
||||||
|
|
||||||
export const useToast = () => {
|
|
||||||
const [message, setMessage] = useState<Message | null>(null);
|
|
||||||
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]);
|
|
||||||
return {
|
|
||||||
setMessage,
|
|
||||||
messageToast,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,15 +1,15 @@
|
|||||||
import { useSupabase } from "@/app/supabase-provider";
|
import { useSupabase } from "@/app/supabase-provider";
|
||||||
|
import { useToast } from "@/lib/hooks/useToast";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { useToast } from "../../../../hooks/useToast";
|
|
||||||
import { isValidUrl } from "../helpers/isValidUrl";
|
import { isValidUrl } from "../helpers/isValidUrl";
|
||||||
|
|
||||||
export const useCrawler = () => {
|
export const useCrawler = () => {
|
||||||
const [isCrawling, setCrawling] = useState(false);
|
const [isCrawling, setCrawling] = useState(false);
|
||||||
const urlInputRef = useRef<HTMLInputElement | null>(null);
|
const urlInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const { setMessage, messageToast } = useToast();
|
|
||||||
const { session } = useSupabase();
|
const { session } = useSupabase();
|
||||||
|
const { publish } = useToast();
|
||||||
if (session === null) {
|
if (session === null) {
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
}
|
}
|
||||||
@ -21,8 +21,8 @@ export const useCrawler = () => {
|
|||||||
|
|
||||||
if (!url || !isValidUrl(url)) {
|
if (!url || !isValidUrl(url)) {
|
||||||
// Assuming you have a function to validate URLs
|
// Assuming you have a function to validate URLs
|
||||||
setMessage({
|
publish({
|
||||||
type: "error",
|
variant: "danger",
|
||||||
text: "Invalid URL",
|
text: "Invalid URL",
|
||||||
});
|
});
|
||||||
setCrawling(false);
|
setCrawling(false);
|
||||||
@ -49,13 +49,13 @@ export const useCrawler = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
setMessage({
|
publish({
|
||||||
type: response.data.type,
|
variant: response.data.type,
|
||||||
text: response.data.message,
|
text: response.data.message,
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setMessage({
|
publish({
|
||||||
type: "error",
|
variant: "danger",
|
||||||
text: "Failed to crawl website: " + JSON.stringify(error),
|
text: "Failed to crawl website: " + JSON.stringify(error),
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@ -66,7 +66,7 @@ export const useCrawler = () => {
|
|||||||
return {
|
return {
|
||||||
isCrawling,
|
isCrawling,
|
||||||
urlInputRef,
|
urlInputRef,
|
||||||
messageToast,
|
|
||||||
crawlWebsite,
|
crawlWebsite,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -2,11 +2,10 @@
|
|||||||
import Button from "@/app/components/ui/Button";
|
import Button from "@/app/components/ui/Button";
|
||||||
import Card from "@/app/components/ui/Card";
|
import Card from "@/app/components/ui/Card";
|
||||||
import Field from "@/app/components/ui/Field";
|
import Field from "@/app/components/ui/Field";
|
||||||
import Toast from "@/app/components/ui/Toast";
|
|
||||||
import { useCrawler } from "./hooks/useCrawler";
|
import { useCrawler } from "./hooks/useCrawler";
|
||||||
|
|
||||||
export const Crawler = (): JSX.Element => {
|
export const Crawler = (): JSX.Element => {
|
||||||
const { urlInputRef, isCrawling, messageToast, crawlWebsite } = useCrawler();
|
const { urlInputRef, isCrawling, crawlWebsite } = useCrawler();
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex justify-center gap-5">
|
<div className="flex justify-center gap-5">
|
||||||
@ -29,7 +28,6 @@ export const Crawler = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Toast ref={messageToast} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useToast } from "@/app/hooks/useToast";
|
|
||||||
import { useSupabase } from "@/app/supabase-provider";
|
import { useSupabase } from "@/app/supabase-provider";
|
||||||
|
import { useToast } from "@/lib/hooks/useToast";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
@ -7,7 +7,7 @@ import { FileRejection, useDropzone } from "react-dropzone";
|
|||||||
|
|
||||||
export const useFileUploader = () => {
|
export const useFileUploader = () => {
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const { messageToast, setMessage } = useToast();
|
const { publish } = useToast();
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const [pendingFileIndex, setPendingFileIndex] = useState<number>(0);
|
const [pendingFileIndex, setPendingFileIndex] = useState<number>(0);
|
||||||
const { session } = useSupabase();
|
const { session } = useSupabase();
|
||||||
@ -31,16 +31,16 @@ export const useFileUploader = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
setMessage({
|
publish({
|
||||||
type: response.data.type,
|
variant: response.data.type,
|
||||||
text:
|
text:
|
||||||
(response.data.type === "success"
|
(response.data.type === "success"
|
||||||
? "File uploaded successfully: "
|
? "File uploaded successfully: "
|
||||||
: "") + JSON.stringify(response.data.message),
|
: "") + JSON.stringify(response.data.message),
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setMessage({
|
publish({
|
||||||
type: "error",
|
variant: "danger",
|
||||||
text: "Failed to upload file: " + JSON.stringify(error),
|
text: "Failed to upload file: " + JSON.stringify(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -50,18 +50,18 @@ export const useFileUploader = () => {
|
|||||||
|
|
||||||
const onDrop = (acceptedFiles: File[], fileRejections: FileRejection[]) => {
|
const onDrop = (acceptedFiles: File[], fileRejections: FileRejection[]) => {
|
||||||
if (fileRejections.length > 0) {
|
if (fileRejections.length > 0) {
|
||||||
setMessage({ type: "error", text: "File too big." });
|
publish({ variant: "danger", text: "File too big." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMessage(null);
|
|
||||||
for (let i = 0; i < acceptedFiles.length; i++) {
|
for (let i = 0; i < acceptedFiles.length; i++) {
|
||||||
const file = acceptedFiles[i];
|
const file = acceptedFiles[i];
|
||||||
const isAlreadyInFiles =
|
const isAlreadyInFiles =
|
||||||
files.filter((f) => f.name === file.name && f.size === file.size)
|
files.filter((f) => f.name === file.name && f.size === file.size)
|
||||||
.length > 0;
|
.length > 0;
|
||||||
if (isAlreadyInFiles) {
|
if (isAlreadyInFiles) {
|
||||||
setMessage({
|
publish({
|
||||||
type: "warning",
|
variant: "warning",
|
||||||
text: `${file.name} was already added`,
|
text: `${file.name} was already added`,
|
||||||
});
|
});
|
||||||
acceptedFiles.splice(i, 1);
|
acceptedFiles.splice(i, 1);
|
||||||
@ -71,9 +71,15 @@ export const useFileUploader = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const uploadAllFiles = async () => {
|
const uploadAllFiles = async () => {
|
||||||
|
if (files.length === 0) {
|
||||||
|
publish({
|
||||||
|
text: "Please, add files to upload",
|
||||||
|
variant: "warning",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsPending(true);
|
setIsPending(true);
|
||||||
setMessage(null);
|
|
||||||
// files.forEach((file) => upload(file));
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
await upload(file);
|
await upload(file);
|
||||||
setPendingFileIndex((i) => i + 1);
|
setPendingFileIndex((i) => i + 1);
|
||||||
@ -96,7 +102,7 @@ export const useFileUploader = () => {
|
|||||||
open,
|
open,
|
||||||
uploadAllFiles,
|
uploadAllFiles,
|
||||||
pendingFileIndex,
|
pendingFileIndex,
|
||||||
messageToast,
|
|
||||||
files,
|
files,
|
||||||
setFiles,
|
setFiles,
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Toast from "@/app/components/ui/Toast";
|
|
||||||
import { AnimatePresence } from "framer-motion";
|
import { AnimatePresence } from "framer-motion";
|
||||||
import Button from "../../../components/ui/Button";
|
import Button from "../../../components/ui/Button";
|
||||||
import Card from "../../../components/ui/Card";
|
import Card from "../../../components/ui/Card";
|
||||||
@ -12,7 +11,6 @@ export const FileUploader = (): JSX.Element => {
|
|||||||
getRootProps,
|
getRootProps,
|
||||||
isDragActive,
|
isDragActive,
|
||||||
isPending,
|
isPending,
|
||||||
messageToast,
|
|
||||||
open,
|
open,
|
||||||
pendingFileIndex,
|
pendingFileIndex,
|
||||||
uploadAllFiles,
|
uploadAllFiles,
|
||||||
@ -21,60 +19,55 @@ export const FileUploader = (): JSX.Element => {
|
|||||||
} = useFileUploader();
|
} = useFileUploader();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<section
|
||||||
<section
|
{...getRootProps()}
|
||||||
{...getRootProps()}
|
className="w-full outline-none flex flex-col gap-5 items-center justify-center p-6"
|
||||||
className="w-full outline-none flex flex-col gap-5 items-center justify-center p-6"
|
>
|
||||||
>
|
<div className="w-full">
|
||||||
<div className="w-full">
|
<div className="flex justify-center gap-5">
|
||||||
<div className="flex justify-center gap-5">
|
{/* Assign a width of 50% to each card */}
|
||||||
{/* Assign a width of 50% to each card */}
|
<div className="w-1/2">
|
||||||
|
<Card className="h-52 flex justify-center items-center">
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<div className="text-center mt-2 p-6 max-w-sm w-full flex flex-col gap-5 items-center">
|
||||||
|
{isDragActive ? (
|
||||||
|
<p className="text-blue-600">Drop the files here...</p>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={open}
|
||||||
|
className="opacity-50 h-full cursor-pointer hover:opacity-100 hover:underline transition-opacity"
|
||||||
|
>
|
||||||
|
Drag and drop files here, or click to browse
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{files.length > 0 && (
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
<Card className="h-52 flex justify-center items-center">
|
<Card className="h-52 py-3 overflow-y-auto">
|
||||||
<input {...getInputProps()} />
|
{files.length > 0 ? (
|
||||||
<div className="text-center mt-2 p-6 max-w-sm w-full flex flex-col gap-5 items-center">
|
<AnimatePresence>
|
||||||
{isDragActive ? (
|
{files.map((file) => (
|
||||||
<p className="text-blue-600">Drop the files here...</p>
|
<FileComponent
|
||||||
) : (
|
key={file.name + file.size}
|
||||||
<button
|
file={file}
|
||||||
onClick={open}
|
setFiles={setFiles}
|
||||||
className="opacity-50 h-full cursor-pointer hover:opacity-100 hover:underline transition-opacity"
|
/>
|
||||||
>
|
))}
|
||||||
Drag and drop files here, or click to browse
|
</AnimatePresence>
|
||||||
</button>
|
) : null}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{files.length > 0 && (
|
|
||||||
<div className="w-1/2">
|
|
||||||
<Card className="h-52 py-3 overflow-y-auto">
|
|
||||||
{files.length > 0 ? (
|
|
||||||
<AnimatePresence>
|
|
||||||
{files.map((file) => (
|
|
||||||
<FileComponent
|
|
||||||
key={file.name + file.size}
|
|
||||||
file={file}
|
|
||||||
setFiles={setFiles}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
) : null}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center justify-center mt-5">
|
|
||||||
<Button isLoading={isPending} onClick={uploadAllFiles}>
|
|
||||||
{isPending
|
|
||||||
? `Uploading ${files[pendingFileIndex].name}`
|
|
||||||
: "Upload"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<div className="flex flex-col items-center justify-center mt-5">
|
||||||
<Toast ref={messageToast} />
|
<Button isLoading={isPending} onClick={uploadAllFiles}>
|
||||||
</>
|
{isPending ? `Uploading ${files[pendingFileIndex].name}` : "Upload"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
10
frontend/lib/hooks/useToast.ts
Normal file
10
frontend/lib/hooks/useToast.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { ToastContext } from "../../app/components/ui/Toast/domain/ToastContext";
|
||||||
|
|
||||||
|
export const useToast = () => {
|
||||||
|
const { publish } = useContext(ToastContext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
publish,
|
||||||
|
};
|
||||||
|
};
|
@ -4,9 +4,3 @@ import { twMerge } from "tailwind-merge";
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
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}`;
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user