Aviso:

Para brindarle información de soporte completa de manera más rápida, el contenido de esta página ha sido traducido al español mediante traducción automática. Para consultar la información de soporte más precisa, consulte la versión en inglés de este contenido.

Configurar el frontend de su aplicación

A continuación, vamos a configurar el frontend de nuestra aplicación.

Instalar y configurar el framework Tailwind CSS

Antes de comenzar a agregar código, necesita asegurarse de que el framework Tailwind le permitirá implementar clases CSS preconstruidas que pueden escanear los archivos HTML y componentes JS, y generar los archivos CSS estáticos requeridos.

Para instalar el framework requerido:

  1. Navegue al directorio PhotoStoreApp/photo-store-app/ y ejecute el siguiente comando
para completar la instalación del framework Tailwind CSS requerido.
copy
$
npm install -D tailwindcss@3.4.17

catalyst_tutorials_photostore_stratus_frontend_tailwind_inst_cmpltd

  1. Ejecute el siguiente comando para crear un archivo tailwind.config.js en su directorio PhotoStoreApp/photo-store-app/.
copy
$
npx tailwindcss init

Se creará un archivo tailwind.config.js. El directorio del proyecto debería verse como la imagen mostrada a continuación: catalyst_tutorials_photostore_stratus_tailwind_front_end_dir

  1. Copie el código dado a continuación y péguelo en el archivo tailwind.config.js.
tailwind.config.js
copy
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}"
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

El archivo tailwind.config.js ha sido configurado para su aplicación.

Instalar paquetes requeridos

Para asegurar que todas las dependencias del frontend se cumplan, necesita instalar los siguientes paquetes:

  • axios: Este paquete le permite manejar solicitudes HTTP.
  • hamburger-react: Este paquete le permite crear un ícono de menú hamburguesa.
  • react-toastify: Este paquete le permite mostrar notificaciones tipo toast.
  • react-router-dom: Este paquete le permite manejar el enrutamiento del lado del cliente.
  • react-icons: Este paquete le permite manejar bibliotecas de íconos populares.

Navegue al directorio PhotoStoreApp/photo-store-app/ y ejecute el siguiente comando en su terminal para instalar los paquetes requeridos.

copy
$
npm install axios hamburger-react react-toastify react-router-dom react-icons

catalyst_tutorials_photostore_stratus_install_react_packs

Codificar su frontend

Vamos a agregar código en los siguientes archivos:

  • En el directorio PhotoStoreApp/photo-store-app/src/:
    • App.js: Este archivo contendrá la lógica para renderizar los componentes de inicio de sesión, todas las operaciones relacionadas con la carga y las funcionalidades de compartir. El código también contiene lógica para la barra de navegación responsiva, mensajes toast y menú hamburguesa.
    • Cree las siguientes carpetas y sus respectivos archivos:
      • service
        • ImageService.js: Contiene funciones auxiliares que definen la lógica para manejar las siguientes funcionalidades:
          • fetchImages(): Obtiene la imagen requerida.
          • fetchSharedImages(): Obtiene la imagen compartida requerida.
          • handleDelete(): Contiene la lógica para eliminar la imagen requerida de la aplicación y del bucket.
          • handleDownload(): Contiene la lógica para descargar la imagen requerida del bucket al sistema local.
          • handleShareAction(): Contiene la lógica para invocar la API /shareDetails para compartir la imagen requerida con otro usuario registrado.
          • handleUpdateSharedDetails(): Contiene la lógica para actualizar los permisos de compartir y los detalles de la imagen.
        • ImageThumbnail.js: Contiene la lógica para renderizar las imágenes cargadas en vista de miniatura, tamaño completo o lista.
      • pages
        • Home.js: Contiene la lógica para renderizar la vista de cuadrícula/lista para las imágenes. Permite operaciones CRUD en las imágenes y soporta vista paginada para mostrar todas las imágenes cargadas en el bucket.
        • ImageGrid.js: Contiene la lógica para renderizar una cuadrícula de todas las imágenes cargadas con opciones para descargar, previsualizar, compartir y eliminar.
        • ImageList.js: Contiene la lógica para renderizar una vista de lista de todas las imágenes cargadas con opciones para descargar, previsualizar, compartir y eliminar.
        • Login.js: Contiene la lógica para dirigir a los usuarios finales a los componentes de inicio de sesión de Catalyst Authentication.
        • Logout.js: Contiene la lógica para cerrar sesión de los usuarios finales en la aplicación.
        • SharedDetails.js: Contiene la lógica para mostrar las imágenes compartidas con otros usuarios de manera paginada. También define la lógica para revocar y actualizar los permisos de acceso de una imagen cuando sea necesario.
        • SharedImageGrid.js: Contiene la lógica para mostrar las imágenes en una cuadrícula responsiva con opciones para descargar la imagen requerida siempre que se otorguen los permisos necesarios.
        • SharedImageList.js: Contiene la misma lógica que SharedImageGrid.js excepto que las imágenes se muestran en vista de lista con imágenes renderizadas como miniaturas.
        • SharedImages.js: Contiene la lógica para mostrar imágenes compartidas en vista de cuadrícula o lista mediante paginación. La lógica de este archivo de código también permite a los usuarios finales registrados y autorizados realizar operaciones CRUD en las imágenes compartidas.
        • Upload.js: Contiene la lógica para cargar una imagen. La lógica en este código está definida para verificar si existe otra imagen y prevenir duplicados. Al cargar la imagen, se hace una llamada a la API /convertToThumbnailAndUpload para convertir la imagen en una miniatura y completar la carga.
    • index.js: Actuará como el punto de entrada para la aplicación.
    • index.css: Necesario para usar Tailwind en nuestra aplicación.
  • En el directorio PhotoStoreApp/photo-store-app/public/:
    • index.html: Este archivo es la plantilla HTML principal para toda la aplicación.

Una vez que se hayan creado todos los archivos para su aplicación, el directorio de su proyecto se verá así: catalyst_tutorials_photostore_stratus_final_dir

Comencemos a agregar código a sus archivos.

Nota: Por favor, revise el código en esta sección para asegurarse de comprenderlo completamente.

Copie el código a continuación y péguelo en los archivos respectivos de su proyecto usando un IDE y guarde los archivos.

App.js
copy
import { useEffect, useState } from "react";
import { Route, Routes, useNavigate } from "react-router-dom";
import Upload from "./pages/Upload";
import "./App.css";
import Login from "./pages/Login";
import Logout from "./pages/Logout";
import { ToastContainer } from "react-toastify";
import { Turn as Hamburger } from "hamburger-react";
import SharedDetails from "./pages/SharedDetails";
import Home from "./pages/Home";
import SharedImages from "./pages/SharedImages";
function HeaderClick() {
  const navigate = useNavigate();
  return (
    <h1
      className="text-2xl font-bold px-4 cursor-pointer hover:text-gray-300 transition duration-300"
      onClick={() => navigate("/")}>
      Photo Store App
    </h1>
  );
}
function App() {
  const [isUserAuthenticated, setIsUserAuthenticated] = useState(false);
  const [isFetching, setIsFetching] = useState(true);
  const [userId, setUserId] = useState(null);
  const [isOpen, setOpen] = useState(false);
  const navigate = useNavigate();
  useEffect(() => {
    const authenticateUser = async () => {
      try {
        const result = await window.catalyst.auth.isUserAuthenticated();
        setUserId(result.content.zuid);
        setIsUserAuthenticated(true);
      } catch (err) {
        console.log("UNAUTHENTICATED");
      } finally {
        setIsFetching(false);
      }
    };
    authenticateUser();
  }, []);
  return (
    <div className="bg-black text-white min-h-screen flex flex-col overflow-hidden">
      <nav className="flex justify-between items-center px-6 py-4 shadow-lg relative">
        <div className="flex items-center relative">
          <Hamburger toggled={isOpen} toggle={setOpen} />
          {isOpen && (
            <div className="absolute left-0 top-full mt-2 bg-gray-800 text-white w-64 h-screen shadow-lg p-4 z-50 overflow-y-auto flex flex-col justify-between text-lg">
              <ul className="flex flex-col space-y-4">
                <li
                  className="py-2 px-4 cursor-pointer hover:bg-blue-700"
                  onClick={() => {
                    setOpen(false);
                    navigate("/upload");
                  }}
                >
                  Upload Image
                </li>
                <li
                  className="py-2 px-4 cursor-pointer hover:bg-blue-700"
                  onClick={() => {
                    setOpen(false);
                    navigate("/");
                  }}
                >
                  Your Gallery
                </li>
                <li
                  className="py-2 px-4 cursor-pointer hover:bg-blue-700"
                  onClick={() => {
                    setOpen(false);
                    navigate("/sharedImages");
                  }}
                >
                  Shared Gallery
                </li>
                <li
                  className="py-2 px-4 cursor-pointer hover:bg-blue-700"
                  onClick={() => {
                    setOpen(false);
                    navigate("/sharedDetails");
                  }}
                >
                  Manage Shared Details
                </li>
              </ul>
              <ul className="pb-16 mt-auto">
                <li
                  className="py-2 px-4 cursor-pointer hover:bg-red-700"
                  onClick={() => {
                    setOpen(false);
                    navigate("/logout");
                  }}
                >
                  LogOut
                </li>
              </ul>
            </div>
          )}
          <HeaderClick />
        </div>
        <ul className="flex space-x-32">
          <li>
            <button
              onClick={() => navigate("/upload")}
              className="bg-blue-800 text-white px-4 py-2 rounded-md hover:bg-blue-900 transition duration-300"
            >
              Upload
            </button>
          </li>
        </ul>
      </nav>
      <div className="flex-grow flex justify-center items-center overflow-hidden">
        {isFetching ? (
          <div className="w-10 h-10 border-4 border-gray-300 border-t-transparent rounded-full animate-spin"></div>
        ) : isUserAuthenticated ? (
          <Routes>
            <Route path="/upload" element=<Upload userId={userId} /> />
            <Route path="/sharedDetails" element=<SharedDetails userId={userId} /> />
            <Route path="/logout" element=<Logout /> />
            <Route path="/" element=<Home userId={userId} /> />
            <Route path="/sharedImages" element=<SharedImages userId={userId} /> />
            <Route path="*" element=<Home userId={userId} /> />
          </Routes>
        ) : (
          <Login />
        )}
      </div>
      <ToastContainer />
    </div>
  );
}
export default App;
View more
ImageService.js
copy
import { toast } from 'react-toastify';
export const fetchImages = async (userId, setImageDetails, setLoading) => {
    try {
        const response = await fetch(`/fetchAllImages?id=${userId}`);
        if (!response.ok) {
            throw new Error("Failed to fetch data");
        }
        const data = await response.json();
        data.map((image) => {
            image.object_url = image.object_url + "?responseCacheControl=max-age=3600";
        });
        setTimeout(() => {
            setLoading(false);
        }, 1000);
        setImageDetails(data || []);
    } catch (err) {
        console.error(err);
        toast.error("Error Occurred", {
            theme: "colored",
        });
    }
};
export const fetchUsers = async (setUsers) => {
    try {
        const response = await fetch("/getAllUsers");
        const data = await response.json();
        setUsers(data);
    } catch (error) {
        console.error("Error fetching users:", error);
        toast.error("Error Occurred", {
            theme: "colored",
        });
    }
};
export const handleDelete = async (imageKey, setImageDetails, setOpenMenuIndex) => {
    try {
        const stratus = window.catalyst.stratus;
        const bucket = stratus.bucket("YOUR_BUCKET_NAME");
        await bucket.deleteObject(imageKey);
        const pathParts = imageKey.split("/");
        const fileName = pathParts.pop();
        const folderName = pathParts.pop();
        const newFileName = fileName.replace(/\.[^/.]+$/, ".jpeg");
        const thumbnailPath = ["photos", "thumbnails", folderName, newFileName].join("/");
        await bucket.deleteObject(thumbnailPath);
        let zcql = window.catalyst.ZCatalystQL;
        let query = `DELETE FROM ImageShareDetails WHERE BucketPath = '${imageKey}'`;
        console.log("QUERY: " + query);
        await zcql.executeQuery(query)
            .then((response) => {
                console.log("ZCQL Response: " + JSON.stringify(response.content));
            })
            .catch((err) => {
                console.log("ZCQL Error: " + err);
            });
        setImageDetails(prev => prev.filter(image => image.key !== imageKey));
        if (setOpenMenuIndex != null) {
            setOpenMenuIndex(null);
        }
        toast.success("Image Deleted Successfully", {
            theme: "colored",
        });
    } catch (error) {
        console.error("Error deleting image:", error);
        toast.error("Error deleting the file. Please try again.", {
            theme: "colored",
        });
    }
};
export const handleDownload = async (imageKey, setOpenMenuIndex) => {
    try {
        const stratus = window.catalyst.stratus;
        const bucket = stratus.bucket("YOUR_BUCKET_NAME");
        const getObject = await bucket.getObject(imageKey);
        const getObjectStart = getObject.start();
        if (setOpenMenuIndex != null) {
            setOpenMenuIndex(null);
        }
        await getObjectStart.then((blob) => {
            const url = URL.createObjectURL(blob.content);
            const a = document.createElement("a");
            a.href = url;
            a.download = imageKey;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }).catch((error) => {
            console.error("Error downloading file:", error);
        });
        setTimeout(() => {
            toast.success("File Downloaded Successfully", {
                theme: "colored",
            });
        }, 2000);
    } catch (error) {
        console.error("Error downloading image:", error);
        toast.error("Error in downloading the file", {
            theme: "colored",
        });
    }
};
export const handleShareAction = async (name, id, path, action, userId) => {
    try {
        const response = await fetch(`/shareDetails`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({ userName: name, imagePath: path, zuid: id, isUpdate: action, sharedBy: userId }),
        });
        const data = await response.json();
        if (data.message === "Access Provided") {
            toast.success("Image Shared Successfully", {
                theme: "colored",
            });
        } else {
            toast.error(data.message, {
                theme: "colored",
            });
        }
        console.log("API Response:", data);
    } catch (error) {
        console.error("Error calling API:", error);
    }
};
export const fetchSharedImages = async (userId, setImageDetails, setLoading) => {
    try {
        const response = await fetch(`/getSharedImages?id=${userId}`);
        if (!response.ok) {
            throw new Error("Failed to fetch data");
        }
        const data = await response.json();
        const flattenedImages = data.flatMap(item => item || []);
        setTimeout(() => {
            setLoading(false);
        }, 1000);
        setImageDetails(flattenedImages || []);
    } catch (err) {
        console.error(err);
        toast.error("Error Occurred", {
            theme: "colored",
        });
    }
};
export const fetchSharedDetails = async (userId, setDetails, setLoading) => {
    try {
        const response = await fetch(`/getSharedDetails?id=${userId}`);
        const data = await response.json();
        setDetails(data);
        setLoading(false);
    } catch (error) {
        console.error("Error fetching details:", error);
        setLoading(false);
    }
};
export const handleUpdateSharedDetails = async (updatedItem, navigate) => {
    try {
        const response = await fetch("/updateSharedDetails", {
            method: "PATCH",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(updatedItem),
        });
        await response.json();
        toast.success("Details Updated Successfully", { theme: "colored" });
        navigate(0);
    } catch (error) {
        console.error("Error updating details:", error);
        toast.error("Error updating details", { theme: "colored" });
    }
};

View more

Nota: Asegúrese de proporcionar el nombre de su bucket en las líneas 38 y 73.
ImageThumbnail.js
copy
import { useState, useEffect } from "react";
export default function ImageThumbnail({ imageUrl, listStyle }) {
    console.log("Image URL: " + imageUrl);
    const [thumbnail, setThumbnail] = useState(null);
    const bucketUrl = "YOUR_BUCKET_URL";
    const pathParts = imageUrl.split("/");
    const fileName = pathParts.pop();
    const folderName = pathParts.pop();
    const newFileName = fileName.replace(/\.[^/.]+$/, ".jpeg");
    const newFilePath = ["photos", "thumbnails", folderName, newFileName].join("/");
    console.log(newFilePath);
    const thumbnailUrl = bucketUrl + newFilePath;
    console.log("ThumbNail URL: " + thumbnailUrl);
    useEffect(() => {
        setThumbnail(newFilePath);
    }, [imageUrl]);
    return (
        <>
            {thumbnail ? (
                listStyle ? (
                    <img
                        src={thumbnailUrl}
                        alt="Thumbnail"
                        style={{ width: "1rem", height: "1rem" }}
                    />
                ) : (
                    <img
                        src={thumbnailUrl}
                        alt="Thumbnail image"
                        className="w-40 h-40 object-cover rounded-lg transition-all duration-500 ease-in-out"
                    />
                )
            ) : (
                <p>Loading...</p>
            )}
        </>
    );
}
View more
Nota: Asegúrese de proporcionar la URL de su bucket en la línea 5.
Home.js
copy
import { useState, useEffect } from 'react';
import { FiX, FiList, FiGrid } from "react-icons/fi"; 
import { fetchImages, handleDelete, handleDownload, fetchUsers, handleShareAction } from "../service/ImageService";
import ImageGrid from './ImageGrid';
import ImageList from './ImageList';
export default function Home({ userId }) {
    const [imageDetails, setImageDetails] = useState([]);
    const [currentPage, setCurrentPage] = useState(1);
    const [openMenuIndex, setOpenMenuIndex] = useState(null);
    const [loading, setLoading] = useState(true);
    const [users, setUsers] = useState([]);
    const [selectedUserIndex, setSelectedUserIndex] = useState(null);
    const [openShareIndex, setOpenShareIndex] = useState(null);
    const [selectedImage, setSelectedImage] = useState(null);
    const [viewMode, setViewMode] = useState("grid"); 
    const imagesPerPage = 9;
    useEffect(() => {
        fetchImages(userId, setImageDetails, setLoading);
    }, [userId]);
    useEffect(() => {
        const handleClickOutside = (event) => {
            if (openShareIndex !== null || selectedUserIndex !== null) {
                setOpenShareIndex(null);
                setSelectedUserIndex(null);
            }
            if (openMenuIndex !== null) {
                setOpenMenuIndex(null);
            }
        };
        document.addEventListener("click", handleClickOutside);
        return () => document.removeEventListener("click", handleClickOutside);
    }, [openShareIndex, selectedUserIndex, openMenuIndex]);
    const handleShareClick = async (index, e) => {
        e.preventDefault();
        e.stopPropagation();
        if (openShareIndex === index) {
            setOpenShareIndex(null);
            setSelectedUserIndex(null);
            return;
        }
        await fetchUsers(setUsers);
        setOpenShareIndex(index);
    };
    const toggleUserOptions = (index, e) => {
        e.preventDefault();
        e.stopPropagation();
        setSelectedUserIndex(selectedUserIndex === index ? null : index);
    };
    const openPreview = (imageUrl) => setSelectedImage(imageUrl);
    const closePreview = () => setSelectedImage(null);
    const indexOfLastImage = currentPage * imagesPerPage;
    const indexOfFirstImage = indexOfLastImage - imagesPerPage;
    const currentImages = imageDetails.slice(indexOfFirstImage, indexOfLastImage);
    const nextPage = () => {
        if (indexOfLastImage < imageDetails.length) setCurrentPage(currentPage + 1);
    };
    const prevPage = () => {
        if (currentPage > 1) setCurrentPage(currentPage - 1);
    };
    const toggleMenu = (index, e) => {
        e.preventDefault();  
        e.stopPropagation(); 
        setOpenMenuIndex(openMenuIndex === index ? null : index); 
    };
    return (
        <div className="min-h-screen bg-black flex flex-col items-center p-6 relative">
            {loading ? (
                <div className="flex justify-center items-center h-screen">
                    <div className="w-10 h-10 border-4 border-gray-300 border-t-transparent rounded-full animate-spin"></div>
                </div>
            ) : (
                <>
                    {imageDetails.length === 0 ? (
                        <div className="flex items-center justify-center h-screen">
                            <p className="text-white text-lg font-semibold text-center">
                                No files found. Click the Upload button to upload your photos.
                            </p>
                        </div>
                    ) : (
                        <>
                            <button
                                className="absolute top-4 right-4 bg-blue-700 text-white px-4 py-2 rounded shadow-lg flex items-center space-x-2"
                                onClick={() => setViewMode(viewMode === "grid" ? "list" : "grid")}
                            >
                                {viewMode === "grid" ? <FiList size={20} /> : <FiGrid size={20} />}
                            </button>
                            <div className="max-w-screen-lg w-full mt-12">
                                {viewMode === "grid" ? (
                                    <ImageGrid
                                        currentImages={currentImages}
                                        openShareIndex={openShareIndex}
                                        handleShareClick={handleShareClick}
                                        users={users}
                                        selectedUserIndex={selectedUserIndex}
                                        toggleUserOptions={toggleUserOptions}
                                        handleShareAction={handleShareAction}
                                        openPreview={openPreview}
                                        openMenuIndex={openMenuIndex}
                                        toggleMenu={toggleMenu}
                                        handleDelete={handleDelete}
                                        handleDownload={handleDownload}
                                        setImageDetails={setImageDetails}
                                        setOpenMenuIndex={setOpenMenuIndex}
                                        setOpenShareIndex={setOpenShareIndex}
                                        setSelectedUserIndex={setSelectedUserIndex}
                                        userId={userId}
                                    />
                                ) : (
                                    <ImageList
                                        currentImages={currentImages}
                                        openShareIndex={openShareIndex}
                                        handleShareClick={handleShareClick}
                                        users={users}
                                        selectedUserIndex={selectedUserIndex}
                                        toggleUserOptions={toggleUserOptions}
                                        handleShareAction={handleShareAction}
                                        handleDelete={handleDelete}
                                        handleDownload={handleDownload}
                                        setImageDetails={setImageDetails}
                                        openPreview={openPreview}
                                        setOpenShareIndex={setOpenShareIndex}
                                        setSelectedUserIndex={setSelectedUserIndex}
                                        userId={userId}
                                    />
                                )}
                            </div>
                            {imageDetails.length > imagesPerPage && (
                                <div className="flex mt-6 justify-center items-center space-x-4">
                                    <button
                                        onClick={prevPage}
                                        disabled={currentPage === 1}
                                        className="px-4 py-2 bg-blue-700 text-white rounded disabled:opacity-50 disabled:bg-blue-200"
                                    >
                                        Previous
                                    </button>
                                    <span className="text-lg font-semibold text-center w-20">
                                        Page {currentPage}
                                    </span>
                                    <button
                                        onClick={nextPage}
                                        disabled={indexOfLastImage >= imageDetails.length}
                                        className="px-4 py-2 bg-blue-700 text-white rounded disabled:opacity-50 disabled:bg-blue-200"
                                    >
                                        Next
                                    </button>
                                </div>
                            )}
                        </>
                    )}
                </>
            )}
            {selectedImage && (
                <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-80 z-50">
                    <div className="relative">
                        <button 
                            className="absolute top-2 right-2 bg-white p-2 rounded-full shadow-md hover:bg-gray-100"
                            onClick={closePreview}
                        >
                            <FiX size={20} className="text-gray-600" />
                        </button>
                        <img
                            src={selectedImage}
                            alt="Preview"
                            className="max-w-full max-h-[50vh] rounded-lg shadow-lg"
                        />
                    </div>
                </div>
            )}
        </div>
    );
}
View more
ImageGrid.js
copy
import { FiShare2, FiChevronRight, FiMoreVertical } from "react-icons/fi";
import ImageThumbnail from "../service/ImageThumbnail";
export default function ImageGrid({
  currentImages,
  openShareIndex,
  handleShareClick,
  users,
  selectedUserIndex,
  toggleUserOptions,
  handleShareAction,
  openPreview,
  openMenuIndex,
  toggleMenu,
  handleDelete,
  handleDownload,
  setImageDetails,
  setOpenMenuIndex,
  setOpenShareIndex,
  setSelectedUserIndex,
  userId
}) {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 place-items-center">
      {currentImages.map((image, index) => (
        <div
          key={image.key}
          className={`flex flex-col items-center w-full max-w-xs relative cursor-pointer ${
            openShareIndex === index ? "" : "transition-transform hover:scale-110"
          }`}
          onClick={() => openPreview(image.object_url)}
        >
          <div className="max-w-xs h-auto flex items-center justify-center bg-gray-200 rounded-lg shadow-lg relative">
            {/* Botón de compartir */}
            <button
              className="absolute top-2 left-2 bg-white p-2 rounded-full shadow-md hover:bg-gray-100"
              onClick={(e) => handleShareClick(index, e)}
            >
              <FiShare2 size={15} className="text-gray-600" />
            </button>
            {/* Menú desplegable de compartir */}
            {openShareIndex === index && users.length > 0 && (
              <div className="absolute top-10 left-2 bg-white text-black border rounded shadow-lg w-40 text-sm z-50">
                {users.map((user, idx) => (
                  <div key={user.id} className="relative">
                    <button
                      className="flex justify-between items-center w-full text-left text-black px-4 py-2 hover:bg-gray-200"
                      onClick={(e) => toggleUserOptions(idx, e)}
                    >
                      {user.name}
                      <FiChevronRight className="text-gray-600" />
                    </button>
                    {selectedUserIndex === idx && (
                      <div
                        className="absolute top-0 left-full ml-2 bg-white text-black border rounded shadow-lg w-32 text-sm z-50"
                        style={{ zIndex: 1000, pointerEvents: "auto" }}
                      >
                        <button
                          className="block w-full px-4 py-2 text-left hover:bg-gray-200"
                          onClick={(e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            handleShareAction(user.name, user.zuid, image.key, false, userId);
                            setSelectedUserIndex(null);
                            setOpenShareIndex(null);
                          }}
                        >
                          View Access
                        </button>
                        <button
                          className="block w-full px-4 py-2 text-left hover:bg-gray-200"
                          onClick={(e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            handleShareAction(user.name, user.zuid, image.key, true, userId);
                            setSelectedUserIndex(null);
                            setOpenShareIndex(null);
                          }}
                        >
                          Edit Access
                        </button>
                      </div>
                    )}
                  </div>
                ))}
              </div>
            )}
            {/* Miniatura de imagen */}
            <ImageThumbnail imageUrl={image.object_url} />
            {/* Botón de menú de opciones */}
            <button
              className="absolute bottom-2 right-2 bg-white p-2 rounded-full shadow-md hover:bg-gray-100"
              onClick={(e) => toggleMenu(index, e)}
            >
              <FiMoreVertical size={15} className="text-gray-600" />
            </button>
            {/* Menú desplegable de opciones */}
            {openMenuIndex === index && (
              <div className="absolute bottom-10 right-2 text-blue-600 bg-white border rounded shadow-lg w-32 text-sm z-50">
                <button
                  className="block w-full px-4 py-2 text-left hover:bg-gray-100"
                  onClick={(e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    handleDownload(image.key, setOpenMenuIndex);
                  }}
                >
                  Download
                </button>
                <button
                  className="block w-full px-4 py-2 text-left text-red-600 hover:bg-gray-100"
                  onClick={(e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    handleDelete(image.key, setImageDetails, setOpenMenuIndex);
                  }}
                >
                  Delete
                </button>
              </div>
            )}
          </div>
          {/* Nombre de la imagen */}
          <div className="w-full max-w-xs mt-3">
            <p className="text-xl font-medium text-white text-center break-words">
              {image.key.split("/").pop()}
            </p>
          </div>
        </div>
      ))}
    </div>
  );
};
View more
ImageList.js
copy
import ImageThumbnail from '../service/ImageThumbnail';
import { FiDownload, FiTrash2, FiEye, FiChevronRight, FiShare2 } from "react-icons/fi";
export default function ImageList({
  currentImages,
  openShareIndex,
  handleShareClick,
  users,
  selectedUserIndex,
  toggleUserOptions,
  handleShareAction,
  handleDelete,
  handleDownload,
  setImageDetails,
  openPreview,
  setOpenShareIndex,
  setSelectedUserIndex,
  userId
}) {
  return (
    <div className="flex flex-col space-y-2">
      {currentImages.map((image, index) => (
        <div
          key={image.key}
          className={`flex items-center w-full bg-gray-800 p-4 rounded-md ${
            openShareIndex === index ? '' : 'transition-transform hover:scale-110'
          }`}
        >
          <ImageThumbnail imageUrl={image.object_url} listStyle={1} />
          <div className="ml-2 flex-1">
            <p className="text-white text-sm font-medium break-words">
              {image.key.split('/').pop()}
            </p>
          </div>
          <button
            className="bg-white p-1 rounded-full shadow-md hover:bg-gray-100 mx-1"
            onClick={() => openPreview(image.object_url)}
          >
            <FiEye size={16} className="text-gray-600" />
          </button>
          <button
            className="bg-white p-1 rounded-full shadow-md hover:bg-gray-100 mx-1"
            onClick={() => handleDownload(image.key, null)}
          >
            <FiDownload size={16} className="text-gray-600" />
          </button>
          <button
            className="bg-white p-1 rounded-full shadow-md hover:bg-red-100"
            onClick={() => handleDelete(image.key, setImageDetails, null)}
          >
            <FiTrash2 size={16} className="text-red-600" />
          </button>
          <button
            className="bg-white p-1 rounded-full shadow-md hover:bg-gray-100 mx-1"
            onClick={(e) => handleShareClick(index, e)}
          >
            <FiShare2 size={15} className="text-gray-600" />
          </button>
          {openShareIndex === index && users.length > 0 && (
            <div className="block top-10 right-4 bg-white text-black border rounded shadow-lg w-40 text-sm z-50">
              {users.map((user, idx) => (
                <div key={user.id} className="relative">
                  <button
                    className="flex justify-between items-center w-full text-left text-black px-4 py-2 hover:bg-gray-200"
                    onClick={(e) => toggleUserOptions(idx, e)}
                  >
                    {user.name}
                    <FiChevronRight className="text-gray-600" />
                  </button>
                  {selectedUserIndex === idx && (
                    <div
                      className="absolute top-0 left-full ml-2 bg-white text-black border rounded shadow-lg w-32 text-sm z-50"
                      style={{ zIndex: 1000, pointerEvents: "auto" }}
                    >
                      <button
                        className="block w-full px-4 py-2 text-left hover:bg-gray-200"
                        onMouseEnter={() => (document.body.style.pointerEvents = "none")}
                        onMouseLeave={() => (document.body.style.pointerEvents = "auto")}
                        onClick={(e) => {
                          e.preventDefault();
                          e.stopPropagation();
                          handleShareAction(user.name, user.zuid, image.key, false, userId);
                          setSelectedUserIndex(null);
                          setOpenShareIndex(null);
                        }}
                      >
                        View Access
                      </button>
                      <button
                        className="block w-full px-4 py-2 text-left hover:bg-gray-200"
                        onMouseEnter={() => (document.body.style.pointerEvents = "none")}
                        onMouseLeave={() => (document.body.style.pointerEvents = "auto")}
                        onClick={(e) => {
                          e.preventDefault();
                          e.stopPropagation();
                          handleShareAction(user.name, user.zuid, image.key, true, userId);
                          setSelectedUserIndex(null);
                          setOpenShareIndex(null);
                        }}
                      >
                        Edit Access
                      </button>
                    </div>
                  )}
                </div>
              ))}
            </div>
          )}
        </div>
      ))}
    </div>
  );
}
View more
Login.js
copy
import { useEffect } from "react";
function Login() {
  useEffect(() => {
    window.location.href = `${window.origin}/__catalyst/auth/login`;
  }, []);
  return null;
}
export default Login;
View more
Logout.js
copy
import { useEffect } from "react";
export default function Logout() {
  useEffect(() => {
    const redirectURL = `${window.origin}/__catalyst/auth/login`;
    const auth = window.catalyst.auth;
    auth.signOut(redirectURL);
  }, []);
  return null;
}
View more
SharedDetails.js
copy
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { fetchSharedDetails, handleUpdateSharedDetails } from "../service/ImageService";
export default function SharedDetails({ userId }) {
    const [details, setDetails] = useState([]);
    const [loading, setLoading] = useState(true);
    const [currentPage, setCurrentPage] = useState(1);
    const rowsPerPage = 10;
    const navigate = useNavigate();
    useEffect(() => {
        fetchSharedDetails(userId, setDetails, setLoading);
    }, [userId]);
    const handleChange = (index, key, value) => {
        const updatedData = [...details];
        updatedData[index][key] = value;
        setDetails(updatedData);
    };
    const indexOfLastRow = currentPage * rowsPerPage;
    const indexOfFirstRow = indexOfLastRow - rowsPerPage;
    const currentRows = details.slice(indexOfFirstRow, indexOfLastRow);
    const totalPages = Math.ceil(details.length / rowsPerPage);
    return (
        <div className="p-6">
            {loading ? (
                <div className="w-10 h-10 border-4 border-gray-300 border-t-transparent rounded-full animate-spin"></div>
            ) : details.length === 0 ? (
                <p className="text-white text-center text-lg font-bold">Nothing Shared</p>
            ) : (
                <>
                    <table className="w-full border-collapse border border-red-400">
                        <thead>
                            <tr className="bg-purple-700">
                                <th className="border text-white border-white px-4 py-2">Username</th>
                                <th className="border text-white border-white px-4 py-2">Image Name</th>
                                <th className="border text-white border-white px-4 py-2">Access Type</th>
                                <th className="border text-white border-white px-4 py-2">Revoke Access</th>
                                <th className="border text-white border-white px-4 py-2">Action</th>
                            </tr>
                        </thead>
                        <tbody>
                            {currentRows.map((item, index) => (
                                <tr key={index} className="text-center">
                                    <td className="border text-white border-white px-4 py-2">{item.UserName}</td>
                                    <td className="border text-white border-white px-4 py-2">
                                        {item.BucketPath.split("/").pop()}
                                    </td>
                                    <td className="border text-white border-white px-4 py-2">
                                        <select
                                            value={item.IsUpdate}
                                            onChange={(e) =>
                                                handleChange(index, "IsUpdate", e.target.value === "true")
                                            }
                                            className="border bg-green-700 rounded px-2 py-1"
                                        >
                                            <option value="true">Edit Access</option>
                                            <option value="false">View Access</option>
                                        </select>
                                    </td>
                                    <td className="border p-2">
                                        <label className="flex items-center justify-center cursor-pointer">
                                            <input
                                                type="checkbox"
                                                className="sr-only"
                                                checked={item.RevokeAccess === "yes"}
                                                onChange={() =>
                                                    handleChange(index, "RevokeAccess", item.RevokeAccess === "yes" ? "no" : "yes")
                                                }
                                            />
                                            <div
                                                className={`relative w-12 h-6 rounded-full transition ${
                                                    item.RevokeAccess === "yes" ? "bg-red-500" : "bg-gray-300"
                                                }`}
                                            >
                                                <div
                                                    className={`absolute left-1 top-1 w-4 h-4 bg-white rounded-full shadow-md transition-transform ${
                                                        item.RevokeAccess === "yes" ? "translate-x-6" : "translate-x-0"
                                                    }`}
                                                ></div>
                                            </div>
                                            <span className="ml-2 text-sm font-medium">
                                                {item.RevokeAccess === "yes" ? "Yes" : "No"}
                                            </span>
                                        </label>
                                    </td>
                                    <td className="border border-white px-4 py-2">
                                        <button
                                            onClick={() => handleUpdateSharedDetails(details[index], navigate)}
                                            className="bg-blue-700 text-white px-4 py-1 rounded hover:bg-blue-900"
                                        >
                                            Update
                                        </button>
                                    </td>
                                </tr>
                            ))}
                        </tbody>
                    </table>
                    {details.length > rowsPerPage && (
                        <div className="flex justify-center mt-4">
                            <button
                                onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
                                disabled={currentPage === 1}
                                className="bg-gray-600 text-white px-3 py-1 mx-2 rounded disabled:opacity-50"
                            >
                                Previous
                            </button>
                            <span className="text-white mx-2">
                                Page {currentPage} of {totalPages}
                            </span>
                            <button
                                onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
                                disabled={currentPage === totalPages}
                                className="bg-gray-600 text-white px-3 py-1 mx-2 rounded disabled:opacity-50"
                            >
                                Next
                            </button>
                        </div>
                    )}
                </>
            )}
        </div>
    );
}
View more
SharedImageGrid.js
copy
import ImageThumbnail from "../service/ImageThumbnail";
import { FiMoreVertical } from "react-icons/fi";
export default function SharedImageGrid({
  currentImages,
  toggleMenu,
  handleDelete,
  handleDownload,
  setImageDetails,
  setOpenMenuIndex,
  openMenuIndex,
  openPreview
}) {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 place-items-center">
      {currentImages.map((image, index) => (
        <div
          key={image.key}
          className="flex flex-col items-center w-full max-w-xs relative cursor-pointer transition-transform hover:scale-110"
          onClick={() => openPreview(image.object_url)}
        >
          <div className="max-w-xs h-auto flex items-center justify-center bg-gray-200 rounded-lg shadow-lg relative">
            <ImageThumbnail imageUrl={image.object_url} />
            <button
              className="absolute bottom-2 right-2 bg-white p-2 rounded-full shadow-md hover:bg-gray-100"
              onClick={(e) => toggleMenu(index, e)}
            >
              <FiMoreVertical size={15} className="text-gray-600" />
            </button>
            {openMenuIndex === index && (
              <div className="absolute bottom-10 right-2 text-blue-600 bg-white border rounded shadow-lg w-32 text-sm z-50">
                {image.isEditAccess && (
                  <button
                    className="block w-full px-4 py-2 text-left text-red-600 hover:bg-gray-100"
                    onClick={(e) => {
                      e.preventDefault();
                      e.stopPropagation();
                      handleDelete(image.key, setImageDetails, setOpenMenuIndex);
                    }}
                  >
                    Delete
                  </button>
                )}
                <button
                  className="block w-full px-4 py-2 text-left hover:bg-gray-100"
                  onClick={(e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    handleDownload(image.key, setOpenMenuIndex);
                  }}
                >
                  Download
                </button>
              </div>
            )}
          </div>
          <div className="w-full max-w-xs mt-3">
            <p className="text-xl font-medium text-white text-center break-words">
              {image.key.split("/").pop()}
            </p>
          </div>
        </div>
      ))}
    </div>
  );
}
View more
SharedImageList.js
copy
import ImageThumbnail from '../service/ImageThumbnail';
import { FiDownload, FiTrash2, FiEye } from "react-icons/fi";
export default function SharedImageList({
  currentImages,
  handleDelete,
  handleDownload,
  setImageDetails,
  openPreview
}) {
  return (
    <div className="flex flex-col space-y-2">
      {currentImages.map((image) => (
        <div
          key={image.key}
          className="flex items-center w-full bg-gray-800 p-4 rounded-md transition-transform hover:scale-110"
        >
          <ImageThumbnail imageUrl={image.object_url} listStyle={1} />
          <div className="ml-2 flex-1">
            <p className="text-white text-sm font-medium break-words">
              {image.key.split('/').pop()}
            </p>
          </div>
          <button
            className="bg-white p-1 rounded-full shadow-md hover:bg-gray-100 mx-1"
            onClick={() => openPreview(image.object_url)}
          >
            <FiEye size={16} className="text-gray-600" />
          </button>
          <button
            className="bg-white p-1 rounded-full shadow-md hover:bg-gray-100 mx-1"
            onClick={() => handleDownload(image.key, null)}
          >
            <FiDownload size={16} className="text-gray-600" />
          </button>
          {image.isEditAccess && (
            <button
              className="bg-white p-1 rounded-full shadow-md hover:bg-red-100"
              onClick={() => handleDelete(image.key, setImageDetails, null)}
            >
              <FiTrash2 size={16} className="text-red-600" />
            </button>
          )}
        </div>
      ))}
    </div>
  );
}
View more
SharedImages.js
copy
import { useState, useEffect } from 'react';
import { FiList, FiGrid, FiX } from "react-icons/fi";
import { fetchSharedImages, handleDelete, handleDownload } from "../service/ImageService";
import SharedImageGrid from './SharedImageGrid';
import SharedImageList from './SharedImageList';
export default function SharedImages({ userId }) {
  const [imageDetails, setImageDetails] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [openMenuIndex, setOpenMenuIndex] = useState(null);
  const [loading, setLoading] = useState(true);
  const [selectedImage, setSelectedImage] = useState(null);
  const [viewMode, setViewMode] = useState("grid");
  const imagesPerPage = 9;
  useEffect(() => {
    fetchSharedImages(userId, setImageDetails, setLoading);
  }, [userId]);
  const indexOfLastImage = currentPage * imagesPerPage;
  const indexOfFirstImage = indexOfLastImage - imagesPerPage;
  const currentImages = imageDetails.slice(indexOfFirstImage, indexOfLastImage);
  const nextPage = () => {
    if (indexOfLastImage < imageDetails.length) setCurrentPage(currentPage + 1);
  };
  const prevPage = () => {
    if (currentPage > 1) setCurrentPage(currentPage - 1);
  };
  const toggleMenu = (index, e) => {
    e.preventDefault();
    e.stopPropagation();
    setOpenMenuIndex(openMenuIndex === index ? null : index);
  };
  const openPreview = (imageUrl) => setSelectedImage(imageUrl);
  const closePreview = () => setSelectedImage(null);
  return (
    <div className="min-h-screen bg-black flex flex-col items-center p-6 relative">
      {loading ? (
        <div className="flex justify-center items-center h-screen">
          <div className="w-10 h-10 border-4 border-gray-300 border-t-transparent rounded-full animate-spin"></div>
        </div>
      ) : (
        <>
          {imageDetails.length === 0 ? (
            <div className="flex items-center justify-center h-screen">
              <p className="text-white text-lg font-semibold text-center">
                No Shared files found.
              </p>
            </div>
          ) : (
            <>
              <button
                className="absolute top-4 right-4 bg-blue-700 text-white px-4 py-2 rounded shadow-lg flex items-center space-x-2"
                onClick={() => setViewMode(viewMode === "grid" ? "list" : "grid")}
              >
                {viewMode === "grid" ? <FiList size={20} /> : <FiGrid size={20} />}
              </button>
              <div className="max-w-screen-lg w-full mt-12">
                {viewMode === "grid" ? (
                  <SharedImageGrid
                    currentImages={currentImages}
                    toggleMenu={toggleMenu}
                    handleDelete={handleDelete}
                    handleDownload={handleDownload}
                    setImageDetails={setImageDetails}
                    setOpenMenuIndex={setOpenMenuIndex}
                    openMenuIndex={openMenuIndex}
                    openPreview={openPreview}
                  />
                ) : (
                  <SharedImageList
                    currentImages={currentImages}
                    handleDelete={handleDelete}
                    handleDownload={handleDownload}
                    setImageDetails={setImageDetails}
                    openPreview={openPreview}
                  />
                )}
              </div>
              {imageDetails.length > imagesPerPage && (
                <div className="flex mt-6 justify-center items-center space-x-4">
                  <button
                    onClick={prevPage}
                    disabled={currentPage === 1}
                    className="px-4 py-2 bg-blue-700 text-white rounded disabled:opacity-50 disabled:bg-blue-200"
                  >
                    Previous
                  </button>
                  <span className="text-lg font-semibold text-center w-20">
                    Page {currentPage}
                  </span>
                  <button
                    onClick={nextPage}
                    disabled={indexOfLastImage >= imageDetails.length}
                    className="px-4 py-2 bg-blue-700 text-white rounded disabled:opacity-50 disabled:bg-blue-200"
                  >
                    Next
                  </button>
                </div>
              )}
            </>
          )}
        </>
      )}
      {selectedImage && (
        <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-80 z-50">
          <div className="relative">
            <button
              className="absolute top-2 right-2 bg-white p-2 rounded-full shadow-md hover:bg-gray-100"
              onClick={closePreview}
            >
              <FiX size={20} className="text-gray-600" />
            </button>
            <img
              src={selectedImage}
              alt="Preview"
              className="max-w-full max-h-[50vh] rounded-lg shadow-lg"
            />
          </div>
        </div>
      )}
    </div>
  );
}
View more
Upload.js
copy
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import { toast } from 'react-toastify';
export default function Upload({ userId }) {
  const [file, setFile] = useState(null);
  const [isUploading, setIsUploading] = useState(false);
  const navigate = useNavigate();
  const zuid = userId;
  const handleFileChange = (e) => {
    const selectedFile = e.target.files[0];
    setFile(selectedFile);
  };
  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      setIsUploading(true);
      const stratus = window.catalyst.stratus;
      const bucket = stratus.bucket("YOUR_BUCKET_NAME");
      const checkObjectAvailability = await bucket.headObject(`photos/${zuid}/${file.name}`);
      console.log("Availability: " + JSON.stringify(checkObjectAvailability));
      if (checkObjectAvailability.content === true) {
        toast.error(`File named ${file.name} already exists`, { theme: "colored" });
        setIsUploading(false);
        setFile(null);
        return;
      }
      const putObject = await bucket.putObject(`photos/${zuid}/${file.name}`, file);
      const response = await putObject.start();
      console.log(JSON.stringify(response));
      const formData = new FormData();
      formData.append("image", file);
      formData.append("id", zuid);
      try {
        console.log("Thumbnail API Started");
        const response = await axios.post("/convertToThumbnailAndUpload", formData);
        console.log("Response: " + JSON.stringify(response));
      } catch (error) {
        console.error("Thumbnail Upload failed", error);
      }
      toast.success(`File uploaded: ${file.name}`, { theme: "colored" });
      navigate("/");
    } catch (error) {
      console.error("Error during upload:", error);
      toast.error("Error uploading the file. Please try again.", { theme: "colored" });
    } finally {
      setIsUploading(false);
      setFile(null);
    }
  };
  return (
    <div className="flex items-center justify-center min-h-screen bg-black">
      <div className="bg-white p-8 rounded-lg shadow-lg w-96 text-center flex flex-col items-center text-black">
        <h1 className="text-2xl font-semibold mb-4 text-black">Upload Image</h1>
        <p className="text-sm text-black mb-4">
          NOTE: Only .png, .jpg, and .jpeg files are allowed.
        </p>
        <form onSubmit={handleSubmit} className="flex flex-col items-center space-y-4 w-full">
          <div className="flex justify-center w-full">
            <input
              type="file"
              onChange={handleFileChange}
              className="border p-2 rounded w-full text-center"
              accept="image/png, image/jpg, image/jpeg"
            />
          </div>
          <button
            type="submit"
            disabled={isUploading}
            className={`px-4 py-2 text-white rounded w-full ${
              isUploading ? "bg-gray-400 cursor-not-allowed" : "bg-blue-500 hover:bg-blue-600"
            }`}
          >
            {isUploading ? "Uploading..." : "Upload"}
          </button>
        </form>
      </div>
    </div>
  );
}
View more
Nota: Asegúrese de proporcionar el nombre de su bucket en la línea 19.
index.js
copy
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { HashRouter as Router } from "react-router-dom";  // Mover Router aquí
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Router>
      <App />
    </Router>
  </React.StrictMode>
);
// Si desea comenzar a medir el rendimiento en su aplicación, pase una función
// para registrar los resultados (por ejemplo: reportWebVitals(console.log))
// o envíe a un endpoint de analíticas. Más información: https://bit.ly/CRA-vitals
reportWebVitals();
View more
index.css
copy
@tailwind base;
@tailwind components;
@tailwind utilities;
View more
index.html
copy
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>PhotoStore App</title>
    <script src="https://static.zohocdn.com/catalyst/sdk/js/4.6.0/catalystWebSDK.js"></script>
    <script src="/__catalyst/sdk/init.js"></script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>
View more

El componente frontend de la aplicación está ahora configurado.

Revisemos rápidamente el funcionamiento de los componentes de función y cliente de la aplicación:

  1. Cuando se activa la URL de la aplicación, dirigimos al usuario a la página de Login. El requisito de autenticación y todas las funcionalidades de inicio de sesión son manejados por Hosted Authentication de Catalyst. Si el usuario final se autentica exitosamente, es redirigido a la página principal de la aplicación.

  2. Una vez que el usuario final es dirigido a la página principal de la aplicación, se invocará el endpoint de la API /fetchAllImages. Esta API devolverá todas las imágenes almacenadas en el bucket y las mostrará como miniaturas en el cliente.

  3. Cuando el usuario final intenta cargar una imagen en la aplicación, la acción de carga de imagen se completará usando el método del SDK web de carga. Mientras se carga la imagen, se invoca el endpoint de la API /convertToThumbnailAndUpload y las imágenes se convierten en miniaturas y se renderizan en el cliente, y la imagen cargada se almacena en la rutaYOUR_BUCKET_NAME/photos/thumbnails/zuid del usuario final conectado”.

  4. La lógica definida para manejar las operaciones de eliminación y descarga está definida en las funciones handleDownload() y handleDelete(). Cuando el usuario final inicia la operación de descarga, la imagen se descargará a su sistema local usando el método del SDK web de descarga. Si la imagen se elimina de la aplicación, también se eliminará de Stratus usando el método del SDK web de eliminación.

  5. La lógica para manejar la función de compartir está definida en handleShareAction(), que invoca la API /shareDetails para compartir la imagen requerida con otro usuario registrado. Cuando el usuario final inicia la función de compartir, se invoca el endpoint de la API /getAllUsers, que devolverá la lista de todos los usuarios finales registrados de la aplicación. Con la lista de usuarios registrados, el usuario final puede elegir compartir la imagen requerida con acceso de View o Edit. El acceso View solo permite al miembro compartido descargar la imagen, y el acceso Edit le permite actualizar, descargar o eliminar la imagen.

Última actualización 2026-03-20 21:51:56 +0530 IST