feat(toast): add global publisher (#177)

This commit is contained in:
Mamadou DICKO 2023-05-27 12:22:19 +02:00 committed by GitHub
parent d44e9e1984
commit e388990384
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 285 additions and 308 deletions

View File

@ -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,
}; };
}; };

View File

@ -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} />
</>
); );
}; };

View File

@ -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} />
</>
); );
}; };

View File

@ -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>
); );
} }

View File

@ -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>
); );
} }

View File

@ -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>
); );
} }

View File

@ -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);
}; };

View File

@ -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>

View File

@ -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;

View 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";

View 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>;
};

View File

@ -0,0 +1,6 @@
import { createContext } from "react";
import { ToastPublisher } from "./types";
const publish: ToastPublisher = () => void 0;
export const ToastContext = createContext({ publish });

View 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;

View File

@ -0,0 +1,5 @@
export const generateToastUniqueId = () => {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 10000);
return `${timestamp}-${random}`;
};

View 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,
};
};

View File

@ -0,0 +1 @@
export * from "./components/index";

View File

@ -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}`,
{ {

View File

@ -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,
};
};

View File

@ -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,
}; };
}; };

View File

@ -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>

View File

@ -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,
}; };

View File

@ -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>
); );
}; };

View 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,
};
};

View File

@ -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}`;
}