お知らせ:

当社は、お客様により充実したサポート情報を迅速に提供するため、本ページのコンテンツは機械翻訳を用いて日本語に翻訳しています。正確かつ最新のサポート情報をご覧いただくには、本内容の英語版を参照してください。

アプリケーションのフロントエンドを設定する

次に、アプリケーションのフロントエンドを設定します。

Tailwind CSSフレームワークのインストールと設定

コードの追加を開始する前に、TailwindフレームワークがHTMLファイルとJSコンポーネントをスキャンし、必要な静的CSSファイルを生成できるビルド済みCSSクラスを実装できるようにする必要があります。

必要なフレームワークをインストールするには:

  1. PhotoStoreApp/photo-store-app/ディレクトリに移動し、以下のコマンドを実行します
必要なTailwind CSSフレームワークのインストールを完了します。
copy
$
npm install -D tailwindcss@3.4.17

catalyst_tutorials_photostore_stratus_frontend_tailwind_inst_cmpltd

  1. 以下のコマンドを実行して、PhotoStoreApp/photo-store-app/ディレクトリにtailwind.config.jsファイルを作成します。
copy
$
npx tailwindcss init

tailwind.config.jsファイルが作成されます。プロジェクトディレクトリは以下の画像のようになります: catalyst_tutorials_photostore_stratus_tailwind_front_end_dir

  1. 以下のコードをコピーして、tailwind.config.jsファイルに貼り付けます。
tailwind.config.js
copy
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}"
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

tailwind.config.jsファイルがアプリケーション用に設定されました。

必要なパッケージのインストール

すべてのフロントエンド依存関係を満たすために、以下のパッケージをインストールする必要があります:

  • axios: このパッケージはHTTPリクエストを処理できます。
  • hamburger-react: このパッケージはハンバーガーメニューアイコンを作成できます。
  • react-toastify: このパッケージはトースト通知を表示できます。
  • react-router-dom: このパッケージはクライアントサイドルーティングを処理できます。
  • react-icons: このパッケージは人気のあるアイコンライブラリを処理できます。

PhotoStoreApp/photo-store-app/ディレクトリに移動し、ターミナルで以下のコマンドを実行して必要なパッケージをインストールします。

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

catalyst_tutorials_photostore_stratus_install_react_packs

フロントエンドのコーディング

以下のファイルにコードを追加します:

  • PhotoStoreApp/photo-store-app/src/ディレクトリ内:
    • App.js: このファイルには、ログインコンポーネントの表示、すべてのアップロード関連操作、共有機能のロジックが含まれます。コードには、レスポンシブナビゲーションバー、トーストメッセージ、ハンバーガーメニューのロジックも含まれています。
    • 以下のフォルダとそれぞれのファイルを作成します:
      • service
        • ImageService.js: 以下の機能を処理するロジックを定義するヘルパー関数が含まれます:
          • fetchImages(): 必要な画像を取得します。
          • fetchSharedImages(): 必要な共有画像を取得します。
          • handleDelete(): アプリケーションとバケットから必要な画像を削除するロジックが含まれます。
          • handleDownload(): バケットからローカルシステムに必要な画像をダウンロードするロジックが含まれます。
          • handleShareAction(): /shareDetails APIを呼び出して、必要な画像を別の登録ユーザーと共有するロジックが含まれます。
          • handleUpdateSharedDetails(): 共有権限と画像の詳細を更新するロジックが含まれます。
        • ImageThumbnail.js: アップロードされた画像をサムネイル、フルスケール、またはリストビューで表示するロジックが含まれます。
      • pages
        • Home.js: 画像のグリッド/リストビューを表示するロジックが含まれます。画像に対するCRUD操作を許可し、バケットにアップロードされたすべての画像を表示するためのページネーションビューをサポートします。
        • ImageGrid.js: アップロードされたすべての画像のグリッドを、ダウンロード、プレビュー、共有、削除のオプション付きで表示するロジックが含まれます。
        • ImageList.js: アップロードされたすべての画像のリストビューを、ダウンロード、プレビュー、共有、削除のオプション付きで表示するロジックが含まれます。
        • Login.js: エンドユーザーをCatalyst Authenticationのログインコンポーネントに誘導するロジックが含まれます。
        • Logout.js: エンドユーザーをアプリケーションからサインアウトさせるロジックが含まれます。
        • SharedDetails.js: 他のユーザーと共有されている画像をページネーション形式で表示するロジックが含まれます。また、必要に応じて画像のアクセス権限の取り消しや更新を行うロジックも定義されています。
        • SharedImageGrid.js: 必要な権限が付与されている場合に、必要な画像をダウンロードするオプション付きのレスポンシブグリッドで画像を表示するロジックが含まれます。
        • SharedImageList.js: SharedImageGrid.jsと同じロジックが含まれますが、画像はサムネイルとして表示されるリストビューで表示されます。
        • SharedImages.js: 共有画像をページネーションを通じてグリッドビューまたはリストビューで表示するロジックが含まれます。このコードファイルのロジックにより、登録済みで権限を持つエンドユーザーは共有画像に対してCRUD操作を実行できます。
        • Upload.js: 画像をアップロードするロジックが含まれます。このコードのロジックは、重複を防ぐために別の画像が存在するかどうかを確認するように定義されています。画像のアップロード中に/convertToThumbnailAndUpload APIが呼び出され、画像がサムネイルに変換され、アップロードが完了します。
    • index.js: アプリケーションのエントリーポイントとして機能します。
    • index.css: アプリケーションでTailwindを使用するために必要です。
  • PhotoStoreApp/photo-store-app/public/ディレクトリ内:
    • index.html: このファイルはアプリケーション全体のメインHTMLテンプレートです。

すべてのファイルがアプリケーション用に作成されると、プロジェクトディレクトリは以下のようになります: catalyst_tutorials_photostore_stratus_final_dir

ファイルへのコードの追加を開始しましょう。

注: このセクションのコードを確認して、十分に理解していることを確認してください。

以下のコードをコピーして、IDEを使用してプロジェクトのそれぞれのファイルに貼り付け、ファイルを保存してください。

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

注: 38行目と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
注: 5行目にバケットのURLを指定してください。
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">
            {/* Share Button */}
            <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>
            {/* Share Dropdown */}
            {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>
            )}
            {/* Image Thumbnail */}
            <ImageThumbnail imageUrl={image.object_url} />
            {/* Options Menu Button */}
            <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>
            {/* Options Menu Dropdown */}
            {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>
          {/* Image Name */}
          <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
注: 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";  // Routerをここに移動
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Router>
      <App />
    </Router>
  </React.StrictMode>
);
// アプリのパフォーマンスを計測するには、結果をログに記録する関数を渡してください
// (例:reportWebVitals(console.log))
// または分析エンドポイントに送信してください。詳細: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

アプリケーションのフロントエンドコンポーネントが設定されました。

アプリケーションのファンクションとクライアントコンポーネントの動作を簡単に確認しましょう:

  1. アプリケーションURLがトリガーされると、ユーザーをLoginページに誘導します。認証要件とすべてのログイン機能は、CatalystのHosted Authenticationによって処理されます。エンドユーザーが正常に認証されると、アプリケーションのメインページにリダイレクトされます。

  2. エンドユーザーがアプリケーションのメインページに移動すると、/fetchAllImages APIエンドポイントが呼び出されます。このAPIはバケットに保存されているすべての画像を返し、クライアントにサムネイルとして表示します。

  3. エンドユーザーがアプリケーションに画像をアップロードしようとすると、アップロードWeb SDKメソッドを使用して画像のアップロードアクションが完了します。画像のアップロード中に/convertToThumbnailAndUpload APIエンドポイントが呼び出され、画像がサムネイルに変換されてクライアントに表示され、アップロードされた画像は「YOUR_BUCKET_NAME/photos/thumbnails/ログインしたエンドユーザーのzuid」のパスに保存されます。

  4. 削除およびダウンロード操作を処理するロジックは、handleDownload()およびhandleDelete()関数で定義されています。エンドユーザーがダウンロード操作を開始すると、ダウンロードWeb SDKメソッドを使用して画像がローカルシステムにダウンロードされます。画像がアプリケーションから削除された場合、削除Web SDKメソッドを使用してStratusからも削除されます。

  5. 共有機能を処理するロジックはhandleShareAction()で定義されており、/shareDetails APIを呼び出して必要な画像を別の登録ユーザーと共有します。エンドユーザーが共有機能を開始すると、/getAllUsers APIエンドポイントが呼び出され、アプリケーションのすべての登録エンドユーザーのリストが返されます。登録ユーザーのリストを使用して、エンドユーザーは必要な画像をViewまたはEditアクセスで共有することを選択できます。Viewアクセスでは共有メンバーは画像のダウンロードのみが許可され、Editアクセスでは更新、ダウンロード、または画像の削除が許可されます。

最終更新日 2026-03-30 13:40:30 +0530 IST