Avatar
Home
Projects
Contact
Available For Work
HomeProjectsContact
Local Time (Africa/Juba)
Local Time (Africa/Juba)
HomeProjectsContact
©2025, All Rights ReservedBuilt with by Maged

Scrolling Through Pixels: The Mogz Visuals Website Saga

A journey through parallax effects, smooth scrolling, and creative problem-solving as we craft a cutting-edge digital presence for Juba's premier media studio.

Jul, 2024Completedmogz.studioSource
Carousel image (1)
Carousel image (2)
Carousel image (3)
Carousel image (4)

Table of Contents

  • The Client and the Vision
  • The Technical Challenge: Smooth Scrolling in Next.js
  • Snippet: scrollContext.tsx
  • Snippet: LocomotiveScrollSection.tsx
  • The Solution: Secure Private Galleries
  • Snippet: useAutoDeleteCookie.ts
  • Snippet: route.ts
  • Snippet: middleware.ts
  • The Feature: On-the-Fly Collection Downloads

The Client and the Vision

In the heart of Juba's thriving media scene, Mogz Visuals has a stellar reputation for high-end photography. Led by founder Jacob Mogga Kei, their work is exceptional, but their online presence didn't yet match the quality of their portfolio. They approached me to build a website that would truly capture the essence of their brand.

The vision was clear. Jacob needed a site that could:

  • Showcase their work in a visually stunning, immersive way.
  • Provide a seamless client experience for accessing and sharing photos.
  • Offer a secure system for private client collections.
  • Include an intuitive download feature for entire photo sets.

This project would become a deep dive into advanced scroll mechanics, secure authentication flows, and on-the-fly file compression, all in service of delivering a cutting-edge digital experience.

The Technical Challenge: Smooth Scrolling in Next.js

To create the immersive feel the client wanted, I decided against a standard portfolio layout. Instead, I opted for a more dynamic experience using parallax effects and smooth scrolling, inspired by the creative implementations on sites like Codrops. This led me to Locomotive Scroll, a powerful library for creating silky-smooth scroll effects.

However, integrating it into a modern Next.js 13 project presented a significant architectural challenge. Locomotive Scroll is a client-side library that wants to take full control of the page's scroll container. This directly conflicts with Next.js's modern App Router, which is designed around server components that render independently of the client-side environment.

The solution was to architect a system that could isolate the client-side library without breaking the server-first paradigm of Next.js. I accomplished this using a React Context Provider.

This ScrollProvider acts as a boundary. It wraps the parts of the application that need smooth scrolling and uses a hook to initialize Locomotive Scroll on the client side. This keeps the server components completely unaware of the library's existence, resolving the core conflict. The provider then uses React Context to pass the scroll instance and its data (like scroll position) down to any child component that needs it.

Share Project

Scrolling Through Pixels: The Mogz Visuals Website Saga

A journey through parallax effects, smooth scrolling, and creative problem-solving as we craft a cutting-edge digital presence for Juba's premier media studio.

Jul, 2024Completedmogz.studioSource
Carousel image (1)
Carousel image (2)
Carousel image (3)
Carousel image (4)

Table of Contents

  • The Client and the Vision
  • The Technical Challenge: Smooth Scrolling in Next.js
  • Snippet: scrollContext.tsx
  • Snippet: LocomotiveScrollSection.tsx
  • The Solution: Secure Private Galleries
  • Snippet: useAutoDeleteCookie.ts
  • Snippet: route.ts
  • Snippet: middleware.ts
  • The Feature: On-the-Fly Collection Downloads

The Client and the Vision

In the heart of Juba's thriving media scene, Mogz Visuals has a stellar reputation for high-end photography. Led by founder Jacob Mogga Kei, their work is exceptional, but their online presence didn't yet match the quality of their portfolio. They approached me to build a website that would truly capture the essence of their brand.

The vision was clear. Jacob needed a site that could:

  • Showcase their work in a visually stunning, immersive way.
  • Provide a seamless client experience for accessing and sharing photos.
  • Offer a secure system for private client collections.
  • Include an intuitive download feature for entire photo sets.

This project would become a deep dive into advanced scroll mechanics, secure authentication flows, and on-the-fly file compression, all in service of delivering a cutting-edge digital experience.

The Technical Challenge: Smooth Scrolling in Next.js

To create the immersive feel the client wanted, I decided against a standard portfolio layout. Instead, I opted for a more dynamic experience using parallax effects and smooth scrolling, inspired by the creative implementations on sites like Codrops. This led me to Locomotive Scroll, a powerful library for creating silky-smooth scroll effects.

However, integrating it into a modern Next.js 13 project presented a significant architectural challenge. Locomotive Scroll is a client-side library that wants to take full control of the page's scroll container. This directly conflicts with Next.js's modern App Router, which is designed around server components that render independently of the client-side environment.

The solution was to architect a system that could isolate the client-side library without breaking the server-first paradigm of Next.js. I accomplished this using a React Context Provider.

This ScrollProvider acts as a boundary. It wraps the parts of the application that need smooth scrolling and uses a hook to initialize Locomotive Scroll on the client side. This keeps the server components completely unaware of the library's existence, resolving the core conflict. The provider then uses React Context to pass the scroll instance and its data (like scroll position) down to any child component that needs it.

Share Project

  • Snippet: useDownloadCollection.ts
  • Lessons Learned
  • Final Thoughts
  • useEffect
    only

    To make implementation easier, I created a simple wrapper component that applies the necessary data-scroll-section attribute, allowing me to designate which parts of the page should be controlled by the scroll library.

    The Solution: Secure Private Galleries

    A critical requirement for Mogz Visuals was a secure portal for clients to view their private photo collections. The system needed to be robust and trustworthy. I engineered a solution using encrypted, auto-expiring session cookies and Next.js middleware.

    The authentication flow works like this:

    • Verification: A client enters their collection ID and password into a form. This data is sent to a server-side API route.
    • Encryption: The API route checks the credentials against the data stored in the Sanity CMS. If they match, it uses CryptoJS to encrypt the unique collection slug and sets it in a secure, HTTP-only cookie with a one-hour expiry.
    • Middleware Protection: A Next.js middleware file is configured to protect all routes under "/private/*". On every request to a private page, the middleware decrypts the cookie and verifies that its slug matches the slug of the requested page. If the cookie is invalid or absent, the user is immediately redirected away from the private content.

    This creates a secure, temporary session for clients without requiring a full user account system. To ensure the session ends properly, a custom hook also removes the cookie when the user closes their browser tab or after the one-hour timer expires.

    The Feature: On-the-Fly Collection Downloads

    To complete the client workflow, I built a feature allowing users to download an entire collection of images as a single zip file. This entire process is handled on the client-side to avoid server load.

    I created a custom hook, useDownloadCollection, that performs several actions:

    • Fetches Images: It takes a list of image URLs and fetches each one as a blob.
    • Zips in Memory: Using the JSZip library, it creates a new zip archive in the browser's memory and adds each image blob to it.
    • Triggers Download: Once all images are added, it generates the final zip file and uses the file-saver library to prompt the user to download it.

    The hook also integrates a simple API-based rate limiter to prevent abuse and adds the user's email to a marketing audience in Resend, helping the client build their mailing list.

    Lessons Learned

    This project was a fantastic learning experience that pushed me to solve several complex, real-world problems. The journey from concept to completion was a deep dive into modern web development practices, and my toolkit is considerably larger for it. Key takeaways include:

    • Integrating Third-Party Libraries: I learned the intricacies of working with DOM-heavy, client-side libraries like Locomotive Scroll within a server-component framework like Next.js. This required creating architectural boundaries with React Context to ensure both parts of the app could function without conflict.
    • Secure Authentication Flows: Building the private gallery system was a practical lesson in security. I mastered the use of encrypted session cookies and middleware to protect routes and manage temporary user access in a secure, stateless way.
    • Client-Side File Manipulation: The download feature was an opportunity to work with file compression and download handling directly in the browser. Using libraries like JSZip to process files on the client-side is a powerful technique for creating performant features that don't overload the server.

    Final Thoughts

    The Mogz Visuals website is more than just a portfolio; it's a digital experience designed to capture the essence of their artistry. The final platform successfully delivered on the client's vision, providing a visually stunning showcase for their work and a secure, seamless portal for their clients.

    This project stands as a testament to what can be achieved when cutting-edge web technologies are combined with creative vision and persistence. For Mogz Visuals, it’s a digital home that truly reflects their artistic prowess and positions them for continued success in Juba’s vibrant media scene.

  • Snippet: useDownloadCollection.ts
  • Lessons Learned
  • Final Thoughts
  • useEffect
    only

    To make implementation easier, I created a simple wrapper component that applies the necessary data-scroll-section attribute, allowing me to designate which parts of the page should be controlled by the scroll library.

    The Solution: Secure Private Galleries

    A critical requirement for Mogz Visuals was a secure portal for clients to view their private photo collections. The system needed to be robust and trustworthy. I engineered a solution using encrypted, auto-expiring session cookies and Next.js middleware.

    The authentication flow works like this:

    • Verification: A client enters their collection ID and password into a form. This data is sent to a server-side API route.
    • Encryption: The API route checks the credentials against the data stored in the Sanity CMS. If they match, it uses CryptoJS to encrypt the unique collection slug and sets it in a secure, HTTP-only cookie with a one-hour expiry.
    • Middleware Protection: A Next.js middleware file is configured to protect all routes under "/private/*". On every request to a private page, the middleware decrypts the cookie and verifies that its slug matches the slug of the requested page. If the cookie is invalid or absent, the user is immediately redirected away from the private content.

    This creates a secure, temporary session for clients without requiring a full user account system. To ensure the session ends properly, a custom hook also removes the cookie when the user closes their browser tab or after the one-hour timer expires.

    The Feature: On-the-Fly Collection Downloads

    To complete the client workflow, I built a feature allowing users to download an entire collection of images as a single zip file. This entire process is handled on the client-side to avoid server load.

    I created a custom hook, useDownloadCollection, that performs several actions:

    • Fetches Images: It takes a list of image URLs and fetches each one as a blob.
    • Zips in Memory: Using the JSZip library, it creates a new zip archive in the browser's memory and adds each image blob to it.
    • Triggers Download: Once all images are added, it generates the final zip file and uses the file-saver library to prompt the user to download it.

    The hook also integrates a simple API-based rate limiter to prevent abuse and adds the user's email to a marketing audience in Resend, helping the client build their mailing list.

    Lessons Learned

    This project was a fantastic learning experience that pushed me to solve several complex, real-world problems. The journey from concept to completion was a deep dive into modern web development practices, and my toolkit is considerably larger for it. Key takeaways include:

    • Integrating Third-Party Libraries: I learned the intricacies of working with DOM-heavy, client-side libraries like Locomotive Scroll within a server-component framework like Next.js. This required creating architectural boundaries with React Context to ensure both parts of the app could function without conflict.
    • Secure Authentication Flows: Building the private gallery system was a practical lesson in security. I mastered the use of encrypted session cookies and middleware to protect routes and manage temporary user access in a secure, stateless way.
    • Client-Side File Manipulation: The download feature was an opportunity to work with file compression and download handling directly in the browser. Using libraries like JSZip to process files on the client-side is a powerful technique for creating performant features that don't overload the server.

    Final Thoughts

    The Mogz Visuals website is more than just a portfolio; it's a digital experience designed to capture the essence of their artistry. The final platform successfully delivered on the client's vision, providing a visually stunning showcase for their work and a secure, seamless portal for their clients.

    This project stands as a testament to what can be achieved when cutting-edge web technologies are combined with creative vision and persistence. For Mogz Visuals, it’s a digital home that truly reflects their artistic prowess and positions them for continued success in Juba’s vibrant media scene.

    scrollContext.tsx

    'use client';
    import { usePathname } from 'next/navigation';
    import {
      ReactNode,
      createContext,
      useContext,
      useEffect,
      useRef,
      useState,
    } from 'react';
    
    // Define the type for the context value, including the scroll instance and functions
    type ScrollContextValue = {
      scrollInstance: LocomotiveScroll | null;
      scroll: number;
      windowHeight: number;
      scrollToSection: (id: string) => void;
    };
    
    // Create a context to hold the scroll-related data and functions
    const ScrollContext = createContext<ScrollContextValue | null>(null);
    
    // Custom hook to use the ScrollContext
    // Throws an error if used outside the ScrollProvider
    export const useScroll = (): ScrollContextValue => {
      const context = useContext(ScrollContext);
      if (!context) {
        throw new Error('useScroll must be used within a ScrollProvider');
      }
      return context;
    };
    
    // ScrollProvider component to wrap the application or part of it
    // Provides scroll-related data and functions to its children
    export const ScrollProvider = ({ children }: { children: ReactNode }) => {
      const scrollRef = useRef<LocomotiveScroll | null>(null); // Ref to hold the LocomotiveScroll instance
      const [scrollPosition, setScrollPosition] = useState(0); // State to track the current scroll position
      const [windowHeight, setWindowHeight] = useState(0); // State to track the window height for scroll limits
      const pathname = usePathname(); // Get the current pathname to reset scroll on navigation
    
      // useEffect to initialize LocomotiveScroll and handle scroll events
      useEffect(() => {
        if (!scrollRef.current) {
          const initializeScroll = async () => {
            const LocomotiveScroll = (await import('locomotive-scroll')).default;
            scrollRef.current = new LocomotiveScroll({
              el: document.querySelector('[data-scroll-container]') as HTMLElement,
              lerp: 0.05,
              smooth: true,
              reloadOnContextChange: true,
              smartphone: { smooth: true },
              touchMultiplier: 3,
            });
    
            // Event listener for scroll events to update the scroll position and window height
            scrollRef.current.on('scroll', (event: any) => {
              setScrollPosition(event.scroll.y);
              if (windowHeight !== event.limit.y) {
                setWindowHeight(event.limit.y);
              }
            });
          };
    
          initializeScroll();
        }
    
        // Cleanup function to destroy the scroll instance when the component unmounts or pathname changes
        return () => {
          if (scrollRef.current) {
            scrollRef.current.destroy();
            scrollRef.current = null;
          }
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [pathname]);
    
      // Function to scroll to a specific section by ID
      const scrollToSection = (id: string) => {
        if (scrollRef.current) {
          scrollRef.current.scrollTo(id);
        }
      };
    
      // Provide the scroll data and functions to the context
      return (
        <ScrollContext.Provider
          value={{
            scroll: scrollPosition,
            scrollToSection,
            windowHeight,
            scrollInstance: scrollRef.current,
          }}
        >
          {children}
        </ScrollContext.Provider>
      );
    };

    LocomotiveScrollSection.tsx

    import { ReactNode } from 'react';
    import { Tag } from '@/lib/types';
    
    // Define the allowed HTML tags for the component
    type SectionTags = Extract<Tag, 'section' | 'div' | 'footer'>;
    
    type LocomotiveScrollWrapperProps = {
      children: ReactNode;  
      Tag?: SectionTags;  
      className?: string;  
      [x: string]: any;  
    };
    
    // Component to wrap content with LocomotiveScroll and dynamic HTML tags
    const LocomotiveScrollSection = ({
      children,
      className,
      Tag = 'section',  // Default tag is 'section'
      ...rest
    }: LocomotiveScrollWrapperProps) => {
      return (
        <Tag
          data-scroll-section 
          className={`overflow-hidden ${className}`} 
          {...rest}  // Spread any additional props
        >
          {children}  
        </Tag>
      );
    };
    
    export default LocomotiveScrollSection;

    scrollContext.tsx

    'use client';
    import { usePathname } from 'next/navigation';
    import {
      ReactNode,
      createContext,
      useContext,
      useEffect,
      useRef,
      useState,
    } from 'react';
    
    // Define the type for the context value, including the scroll instance and functions
    type ScrollContextValue = {
      scrollInstance: LocomotiveScroll | null;
      scroll: number;
      windowHeight: number;
      scrollToSection: (id: string) => void;
    };
    
    // Create a context to hold the scroll-related data and functions
    const ScrollContext = createContext<ScrollContextValue | null>(null);
    
    // Custom hook to use the ScrollContext
    // Throws an error if used outside the ScrollProvider
    export const useScroll = (): ScrollContextValue => {
      const context = useContext(ScrollContext);
      if (!context) {
        throw new Error('useScroll must be used within a ScrollProvider');
      }
      return context;
    };
    
    // ScrollProvider component to wrap the application or part of it
    // Provides scroll-related data and functions to its children
    export const ScrollProvider = ({ children }: { children: ReactNode }) => {
      const scrollRef = useRef<LocomotiveScroll | null>(null); // Ref to hold the LocomotiveScroll instance
      const [scrollPosition, setScrollPosition] = useState(0); // State to track the current scroll position
      const [windowHeight, setWindowHeight] = useState(0); // State to track the window height for scroll limits
      const pathname = usePathname(); // Get the current pathname to reset scroll on navigation
    
      // useEffect to initialize LocomotiveScroll and handle scroll events
      useEffect(() => {
        if (!scrollRef.current) {
          const initializeScroll = async () => {
            const LocomotiveScroll = (await import('locomotive-scroll')).default;
            scrollRef.current = new LocomotiveScroll({
              el: document.querySelector('[data-scroll-container]') as HTMLElement,
              lerp: 0.05,
              smooth: true,
              reloadOnContextChange: true,
              smartphone: { smooth: true },
              touchMultiplier: 3,
            });
    
            // Event listener for scroll events to update the scroll position and window height
            scrollRef.current.on('scroll', (event: any) => {
              setScrollPosition(event.scroll.y);
              if (windowHeight !== event.limit.y) {
                setWindowHeight(event.limit.y);
              }
            });
          };
    
          initializeScroll();
        }
    
        // Cleanup function to destroy the scroll instance when the component unmounts or pathname changes
        return () => {
          if (scrollRef.current) {
            scrollRef.current.destroy();
            scrollRef.current = null;
          }
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [pathname]);
    
      // Function to scroll to a specific section by ID
      const scrollToSection = (id: string) => {
        if (scrollRef.current) {
          scrollRef.current.scrollTo(id);
        }
      };
    
      // Provide the scroll data and functions to the context
      return (
        <ScrollContext.Provider
          value={{
            scroll: scrollPosition,
            scrollToSection,
            windowHeight,
            scrollInstance: scrollRef.current,
          }}
        >
          {children}
        </ScrollContext.Provider>
      );
    };

    LocomotiveScrollSection.tsx

    import { ReactNode } from 'react';
    import { Tag } from '@/lib/types';
    
    // Define the allowed HTML tags for the component
    type SectionTags = Extract<Tag, 'section' | 'div' | 'footer'>;
    
    type LocomotiveScrollWrapperProps = {
      children: ReactNode;  
      Tag?: SectionTags;  
      className?: string;  
      [x: string]: any;  
    };
    
    // Component to wrap content with LocomotiveScroll and dynamic HTML tags
    const LocomotiveScrollSection = ({
      children,
      className,
      Tag = 'section',  // Default tag is 'section'
      ...rest
    }: LocomotiveScrollWrapperProps) => {
      return (
        <Tag
          data-scroll-section 
          className={`overflow-hidden ${className}`} 
          {...rest}  // Spread any additional props
        >
          {children}  
        </Tag>
      );
    };
    
    export default LocomotiveScrollSection;

    useDownloadCollection.ts

    import JSZip from 'jszip';
    import { useState } from 'react';
    import { saveAs } from 'file-saver';
    import { useToast } from '../context/ToastContext';
    import { fetchSanityData } from '../sanity/client';
    import { getPrivateCollectionGallery } from '../sanity/queries';
    import { COLLECTION } from '../types';
    
    const useDownloadCollection = ({ title, uniqueId, gallery }: COLLECTION) => {
      const [loading, setLoading] = useState(false);
      const { show } = useToast();
    
      const folderName = `[MOGZ] ${title}`;
      const zip = new JSZip();
      const folder = zip.folder(folderName);
    
      const showToast = (
        message: string,
        status: 'success' | 'error',
        autoClose: boolean = true
      ) => {
        show(message, { status, autoClose });
      };
    
      const checkRateLimit = async (id: string): Promise<boolean> => {
        const response = await fetch(`/api/rateLimit?id=${id}`, {
          method: 'GET',
        });
        if (!response.ok) {
          const { message } = await response.json();
          console.log('Rate limit status:', response.status, message);
          showToast('Rate limit exceeded, please try again later.', 'error', false);
          return false;
        }
        return true;
      };
    
      const addEmailToAudience = async (email: string) => {
        try {
          const response = await fetch('/api/contact/audience', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({ email }),
          });
          console.log(response);
        } catch (error) {
          console.log(error);
        }
      };
    
      const fetchImages = async () => {
        const gallery: string[] = await fetchSanityData(
          getPrivateCollectionGallery,
          { id: uniqueId }
        );
        return gallery;
      };
    
      const downloadImages = async (email: string) => {
        setLoading(true);
        let images = gallery;
        try {
          if (!(await checkRateLimit('download'))) return;
    
          await addEmailToAudience(email);
    
          if (!images) {
            images = await fetchImages();
          }
    
          const imageFetchPromises = images.map(async (image, index) => {
            try {
              const response = await fetch(image);
              if (!response.ok) {
                throw new Error(
                  `Failed to fetch image at index ${index}, status: ${response.status}`
                );
              }
              const blob = await response.blob();
              if (!folder) {
                throw new Error('folder is undefined');
              }
              folder.file(generateImageName(title, index), blob, { binary: true });
            } catch (err) {
              console.error(`Error fetching image at index ${index}:`, err);
              throw err;
            }
          });
    
          await Promise.all(imageFetchPromises);
    
          console.log('Adding images done, proceeding to ZIP...');
          const content = await zip.generateAsync({ type: 'blob' });
    
          saveAs(content, `${folderName}.zip`);
          showToast('Collection downloaded successfully!', 'success');
        } catch (err: any) {
          console.error(err);
          showToast(
            `An error occurred while downloading the collection! Try again later.`,
            'error'
          );
        } finally {
          setLoading(false);
        }
      };
    
      return {
        loading,
        downloadImages,
      };
    };
    
    export default useDownloadCollection;
    
    const generateImageName = (title: string, index: number): string => {
      const formattedTitle = title.replace(/\s/g, '-');
      return `[MOGZ]-${formattedTitle}-${index + 1}.jpg`;
    };
    

    useDownloadCollection.ts

    import JSZip from 'jszip';
    import { useState } from 'react';
    import { saveAs } from 'file-saver';
    import { useToast } from '../context/ToastContext';
    import { fetchSanityData } from '../sanity/client';
    import { getPrivateCollectionGallery } from '../sanity/queries';
    import { COLLECTION } from '../types';
    
    const useDownloadCollection = ({ title, uniqueId, gallery }: COLLECTION) => {
      const [loading, setLoading] = useState(false);
      const { show } = useToast();
    
      const folderName = `[MOGZ] ${title}`;
      const zip = new JSZip();
      const folder = zip.folder(folderName);
    
      const showToast = (
        message: string,
        status: 'success' | 'error',
        autoClose: boolean = true
      ) => {
        show(message, { status, autoClose });
      };
    
      const checkRateLimit = async (id: string): Promise<boolean> => {
        const response = await fetch(`/api/rateLimit?id=${id}`, {
          method: 'GET',
        });
        if (!response.ok) {
          const { message } = await response.json();
          console.log('Rate limit status:', response.status, message);
          showToast('Rate limit exceeded, please try again later.', 'error', false);
          return false;
        }
        return true;
      };
    
      const addEmailToAudience = async (email: string) => {
        try {
          const response = await fetch('/api/contact/audience', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({ email }),
          });
          console.log(response);
        } catch (error) {
          console.log(error);
        }
      };
    
      const fetchImages = async () => {
        const gallery: string[] = await fetchSanityData(
          getPrivateCollectionGallery,
          { id: uniqueId }
        );
        return gallery;
      };
    
      const downloadImages = async (email: string) => {
        setLoading(true);
        let images = gallery;
        try {
          if (!(await checkRateLimit('download'))) return;
    
          await addEmailToAudience(email);
    
          if (!images) {
            images = await fetchImages();
          }
    
          const imageFetchPromises = images.map(async (image, index) => {
            try {
              const response = await fetch(image);
              if (!response.ok) {
                throw new Error(
                  `Failed to fetch image at index ${index}, status: ${response.status}`
                );
              }
              const blob = await response.blob();
              if (!folder) {
                throw new Error('folder is undefined');
              }
              folder.file(generateImageName(title, index), blob, { binary: true });
            } catch (err) {
              console.error(`Error fetching image at index ${index}:`, err);
              throw err;
            }
          });
    
          await Promise.all(imageFetchPromises);
    
          console.log('Adding images done, proceeding to ZIP...');
          const content = await zip.generateAsync({ type: 'blob' });
    
          saveAs(content, `${folderName}.zip`);
          showToast('Collection downloaded successfully!', 'success');
        } catch (err: any) {
          console.error(err);
          showToast(
            `An error occurred while downloading the collection! Try again later.`,
            'error'
          );
        } finally {
          setLoading(false);
        }
      };
    
      return {
        loading,
        downloadImages,
      };
    };
    
    export default useDownloadCollection;
    
    const generateImageName = (title: string, index: number): string => {
      const formattedTitle = title.replace(/\s/g, '-');
      return `[MOGZ]-${formattedTitle}-${index + 1}.jpg`;
    };
    
    import Cookies from 'js-cookie';
    import { useEffect, useState } from 'react';
    import { useRouter } from 'next/navigation';
    import { useToast } from '../context/ToastContext';
    
    export const useAutoDeleteCookie = (slug: string, isPrivate: boolean) => {
      const [decryptedSlug, setDecryptedSlug] = useState<string | null>(null);
      const { show } = useToast();
      const router = useRouter();
    
      useEffect(() => {
        if (isPrivate) {
          const func = async () => {
            const encryptedCookie = Cookies.get('collectionAccess');
            if (encryptedCookie) {
              const parsedCookie = JSON.parse(encryptedCookie);
              console.log('encrypted slug', parsedCookie.slug);
              const decryptedSlug = await getDecryptedSlug(parsedCookie.slug);
    
              setDecryptedSlug(decryptedSlug);
            }
          };
          // Decrypt the cookie and set the state
          func();
        }
      }, [isPrivate]);
    
      useEffect(() => {
        if (isPrivate && decryptedSlug) {
          const timer = setTimeout(() => {
            if (slug === decryptedSlug) {
              Cookies.remove('collectionAccess');
              router.push('/gallery');
              show('Your access to private collection expired!', {
                status: 'info',
                autoClose: false,
              });
            }
          }, 1 * 60 * 60 * 1000); // 1 hour in milliseconds
    
          // Listen to the 'beforeunload' event to delete the cookie when the tab is closed
          const handleBeforeUnload = async () => {
            if (slug === decryptedSlug) {
              Cookies.remove('collectionAccess');
            }
          };
          window.addEventListener('beforeunload', handleBeforeUnload);
    
          return () => {
            clearTimeout(timer);
            window.removeEventListener('beforeunload', handleBeforeUnload);
          };
        }
      }, [decryptedSlug, isPrivate, router, show, slug]);
    };
    
    const getDecryptedSlug = async (encryptedCookie: string) => {
      const response = await fetch('/api/decryptCookie', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ encryptedCookie }),
      });
      const { decryptedSlug } = await response.json();
    
      return decryptedSlug;
    };
    import Cookies from 'js-cookie';
    import { useEffect, useState } from 'react';
    import { useRouter } from 'next/navigation';
    import { useToast } from '../context/ToastContext';
    
    export const useAutoDeleteCookie = (slug: string, isPrivate: boolean) => {
      const [decryptedSlug, setDecryptedSlug] = useState<string | null>(null);
      const { show } = useToast();
      const router = useRouter();
    
      useEffect(() => {
        if (isPrivate) {
          const func = async () => {
            const encryptedCookie = Cookies.get('collectionAccess');
            if (encryptedCookie) {
              const parsedCookie = JSON.parse(encryptedCookie);
              console.log('encrypted slug', parsedCookie.slug);
              const decryptedSlug = await getDecryptedSlug(parsedCookie.slug);
    
              setDecryptedSlug(decryptedSlug);
            }
          };
          // Decrypt the cookie and set the state
          func();
        }
      }, [isPrivate]);
    
      useEffect(() => {
        if (isPrivate && decryptedSlug) {
          const timer = setTimeout(() => {
            if (slug === decryptedSlug) {
              Cookies.remove('collectionAccess');
              router.push('/gallery');
              show('Your access to private collection expired!', {
                status: 'info',
                autoClose: false,
              });
            }
          }, 1 * 60 * 60 * 1000); // 1 hour in milliseconds
    
          // Listen to the 'beforeunload' event to delete the cookie when the tab is closed
          const handleBeforeUnload = async () => {
            if (slug === decryptedSlug) {
              Cookies.remove('collectionAccess');
            }
          };
          window.addEventListener('beforeunload', handleBeforeUnload);
    
          return () => {
            clearTimeout(timer);
            window.removeEventListener('beforeunload', handleBeforeUnload);
          };
        }
      }, [decryptedSlug, isPrivate, router, show, slug]);
    };
    
    const getDecryptedSlug = async (encryptedCookie: string) => {
      const response = await fetch('/api/decryptCookie', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ encryptedCookie }),
      });
      const { decryptedSlug } = await response.json();
    
      return decryptedSlug;
    };