diff --git a/README.md b/README.md index 67cfc344..0ba9e4e0 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ [![Deploy](https://github.com/la-famiglia-jst2324/parma-web/actions/workflows/release.yml/badge.svg?event=release)](https://parma.software) [![Deploy](https://github.com/la-famiglia-jst2324/parma-web/actions/workflows/release.yml/badge.svg?event=push)](https://staging.parma.software) [![Major Tag](https://github.com/la-famiglia-jst2324/parma-web/actions/workflows/tag-major.yml/badge.svg)](https://github.com/la-famiglia-jst2324/parma-web/actions/workflows/tag-major.yml) -![Functions](https://img.shields.io/badge/functions-21.63%25-red.svg?style=flat) -![Lines](https://img.shields.io/badge/lines-17.84%25-red.svg?style=flat) +![Functions](https://img.shields.io/badge/functions-17.75%25-red.svg?style=flat) +![Lines](https://img.shields.io/badge/lines-16.2%25-red.svg?style=flat) ParmaAI webstack including frontend and REST API backend. diff --git a/src/app/buckets/[id]/page.tsx b/src/app/buckets/[id]/page.tsx new file mode 100644 index 00000000..982e77de --- /dev/null +++ b/src/app/buckets/[id]/page.tsx @@ -0,0 +1,232 @@ +'use client' + +import { useEffect, useState } from 'react' +import type { Company, Bucket } from '@prisma/client' +import { Text, Button, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '@tremor/react' +import { useRouter } from 'next/navigation' +import { PencilIcon, ShareIcon, TrashIcon } from '@heroicons/react/20/solid' +import { GoBackButton } from '@/components/GoBackButton' +import EditBucketModal from '@/components/buckets/EditBucketModal' +import { Popup } from '@/components/Popup' +import { PopupType } from '@/types/popup' +import DeleteBucketModal from '@/components/buckets/DeleteBucketModal' +import BucketFunctions from '@/app/services/bucket.service' +import type { ShareBucketProps } from '@/components/buckets/ShareBucketModal' +import ShareBucketModal from '@/components/buckets/ShareBucketModal' +import { MainLayout } from '@/components/MainLayout' + +const initialBucketValue = { + id: 0, + title: '', + description: '', + isPublic: true, + ownerId: 0, + createdAt: new Date(), + modifiedAt: new Date() +} + +export default function BucketPage({ params: { id } }: { params: { id: string } }) { + const router = useRouter() + const [bucket, setBucket] = useState(initialBucketValue) + const [bucketCompanies, setBucketCompanies] = useState() + const [isShareModalOpen, setIsShareModalOpen] = useState(false) + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) + const [isEditModalOpen, setIsEditModalOpen] = useState(false) + const [showSuccess, setShowSuccess] = useState(false) + const [showError, setShowError] = useState(false) + const [popupText, setPopupText] = useState('') + + useEffect(() => { + BucketFunctions.getBucketById(+id) + .then((data) => { + setBucket(data) + BucketFunctions.getCompaniesForBucket(+id) + .then((res) => { + setBucketCompanies(res) + }) + .catch((e) => { + console.log(e) + }) + }) + .catch((e) => { + console.log(e) + }) + }, [id]) + + const toggleDeleteModal = () => { + setIsDeleteModalOpen((val) => !val) + } + const toggleEditModal = () => { + setIsEditModalOpen((val) => !val) + } + const toggleShareModal = () => { + setIsShareModalOpen((val) => !val) + } + + const saveBucket = (title: string, description: string | null, isPublic: boolean) => { + BucketFunctions.updateBucket(title, description, +id, isPublic) + .then((res) => { + if (res) { + setBucket(res) + setShowSuccess(true) + setTimeout(() => setShowSuccess(false), 3000) + setPopupText('Bucket is updated successfully') + } else { + setShowError(true) + setTimeout(() => setShowError(false), 3000) + setPopupText('Failed to update bucket') + } + }) + .catch((e) => { + setShowError(true) + setTimeout(() => setShowError(false), 3000) + setPopupText(e) + }) + } + + const onDeleteBucket = () => { + BucketFunctions.deleteBucket(+id) + .then((res) => { + if (res) { + setPopupText('Bucket is deleted successfully') + setShowSuccess(true) + setTimeout(() => { + setShowSuccess(false) + router.push('/buckets') + }, 1500) + } + }) + .catch((error) => { + setPopupText(error) + setShowError(true) + setTimeout(() => setShowError(false), 3000) + }) + setIsDeleteModalOpen(false) + } + + const onHandleShare = (shareUsersList: ShareBucketProps[]) => { + shareUsersList.forEach((user) => { + BucketFunctions.shareBucket(user) + .then((res) => { + if (res) { + setPopupText('Bucket is shared successfully') + setShowSuccess(true) + setIsShareModalOpen(false) + setTimeout(() => { + setShowSuccess(false) + }, 3000) + } + }) + .catch((e) => { + setPopupText(e) + setShowError(true) + setTimeout(() => { + setShowError(false) + setIsShareModalOpen(false) + }, 3000) + }) + }) + } + return ( + +
+
+
+
+
+ +
+
+

{bucket.title}

+
+
+
+ + {isShareModalOpen && ( + onHandleShare(shareUsersList)} + handleClose={toggleShareModal} + > + )} + + {isEditModalOpen && ( + + saveBucket(title, description, isPublic) + } + > + )} + + {isDeleteModalOpen && ( + + )} +
+
+
+

{bucket.description}

+
+ +
+

Companies in this bucket

+
+
+ + {bucketCompanies && bucketCompanies.length > 0 && ( + + + + Company Name + Description + + + + {bucketCompanies?.map((item) => ( + + {item.name} + + {item.description} + + + ))} + +
+ )} + + {bucketCompanies && !(bucketCompanies.length > 0) && ( +
This bucket does not have any companies.
+ )} +
+ {showSuccess && } + {showError && } +
+
+ ) +} diff --git a/src/app/buckets/add-bucket/page.tsx b/src/app/buckets/add-bucket/page.tsx new file mode 100644 index 00000000..ee3b9fec --- /dev/null +++ b/src/app/buckets/add-bucket/page.tsx @@ -0,0 +1,155 @@ +'use client' + +import { Button, Switch, MultiSelect, MultiSelectItem } from '@tremor/react' +import type { FormEvent } from 'react' +import { useEffect, useState } from 'react' +import type { Bucket, Company } from '@prisma/client' +import { useRouter } from 'next/navigation' +import { CheckBadgeIcon } from '@heroicons/react/20/solid' +import { FormContent } from '@/components/FormContent' +import { GoBackButton } from '@/components/GoBackButton' +import { Popup } from '@/components/Popup' +import { PopupType } from '@/types/popup' +import BucketFunctions from '@/app/services/bucket.service' +import { MainLayout } from '@/components/MainLayout' + +interface CompaniesPaginated { + companies: Company[] + pagination: { + currentPage: number + pageSize: number + totalPages: number + totalCount: number + } +} +export default function AddBucketPage() { + const [allCompaniesPaginated, setCompaniesPaginated] = useState({ + companies: [], + pagination: { + currentPage: 1, + pageSize: 10, + totalPages: 0, + totalCount: 0 + } + }) + // Bucket props + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [isPublic, setIsPublic] = useState(true) + const [selectedCompanies, setSelectedCompanies] = useState([]) + const [showSuccess, setShowSuccess] = useState(false) + const [showError, setShowError] = useState(false) + const router = useRouter() + + const handleSwitchChange = (value: boolean) => { + setIsPublic(value) + } + + useEffect(() => { + BucketFunctions.getAllCompanies() + .then(setCompaniesPaginated) + .catch((error) => { + console.error('Failed to fetch companies:', error) + }) + }, []) + + const createBucket = async (event: FormEvent) => { + event.preventDefault() + const formData = new FormData(event.currentTarget) + const bucket = { + title: formData.get('title') as string, + isPublic, + description: formData.get('description') as string + } + + BucketFunctions.createBucket(bucket) + .then((data: Bucket) => { + // Add companies to the bucket + if (selectedCompanies.length > 0) { + BucketFunctions.addCompaniesToBucket(data.id, selectedCompanies) + .then((data) => console.log(data)) + .catch((e) => console.log(e)) + } + setShowSuccess(true) + setTimeout(() => { + router.push('/buckets') + setShowSuccess(false) + }, 1500) // Remove it from the screen + }) + .catch((error) => { + setShowError(true) + setTimeout(() => setShowError(false), 1500) // Remove it from the screen + console.error('Error:', error) + }) + } + return ( + +
+
+
+
+ +
+
+

Create Bucket

+

+ You can create a collection of companies here. Please choose companies from a list of available + companies. +

+
+
+
+
+ setTitle(e.target.value)} + /> +
+
+ setDescription(e.target.value)} + /> +
+
+ + setSelectedCompanies(e || [])}> + {allCompaniesPaginated?.companies.map((company) => ( + + {company.name} + + ))} + +
+ +
+
Make this bucket public
+ +
+
+ +
+
+
+ {showSuccess && ( + + )} + {showError && } +
+
+ ) +} diff --git a/src/app/buckets/page.tsx b/src/app/buckets/page.tsx new file mode 100644 index 00000000..05582496 --- /dev/null +++ b/src/app/buckets/page.tsx @@ -0,0 +1,165 @@ +'use client' +import { Button, TextInput } from '@tremor/react' +import { useEffect, useState } from 'react' +import Link from 'next/link' +import type { Bucket } from '@prisma/client' +import { PlusCircleIcon } from '@heroicons/react/20/solid' +import BucketCard from '@/components/buckets/bucketCard' +import BucketFunctions from '@/app/services/bucket.service' +import { MainLayout } from '@/components/MainLayout' + +interface BucketsPaginated { + buckets: Bucket[] + pagination: { + currentPage: number + pageSize: number + totalPages: number + totalCount: number + } +} + +export default function BucketsPage() { + // Need an api call to get myBuckets + const myBuckets: Bucket[] = [ + { + id: 1, + title: 'Bucket 1', + description: + 'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Iusto maiores ipsum eum quae ad architecto voluptatem illum name facere et!', + isPublic: true, + ownerId: 1, + createdAt: new Date(), + modifiedAt: new Date() + }, + { + id: 2, + title: 'Bucket 2', + description: + 'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Iusto maiores ipsum eum quae ad architecto voluptatem illum name facere et!', + isPublic: false, + ownerId: 2, + createdAt: new Date(), + modifiedAt: new Date() + }, + { + id: 3, + title: 'Bucket 3', + description: + 'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Iusto maiores ipsum eum quae ad architecto voluptatem illum name facere et!', + isPublic: true, + ownerId: 3, + createdAt: new Date(), + modifiedAt: new Date() + } + ] + + // Here we will manage the buckets that comes from backend + const [allBuckets, setAllBuckets] = useState() + const [clearSearchDisable, setClearSearchDisable] = useState(true) + const [searchTerm, setSearchTerm] = useState('') + const [page, setPage] = useState(1) + + useEffect(() => { + // This will change when the backend changes (after midterm review) + BucketFunctions.getAllBuckets(page) + .then((res: BucketsPaginated) => { + if (allBuckets) { + const moreBuckets = { + buckets: [...allBuckets.buckets, ...res.buckets], + pagination: allBuckets?.pagination + } + setAllBuckets(moreBuckets) + } else { + setAllBuckets(res) + } + }) + .catch((e) => { + console.log(e) + }) + }, [page]) + + useEffect(() => { + if (searchTerm === '') { + BucketFunctions.getAllBuckets(1) + .then((res: BucketsPaginated) => { + setAllBuckets(res) + }) + .catch((e) => { + console.log(e) + }) + } + }, [searchTerm]) + + const filterBuckets = (searchTerm: string) => { + if (searchTerm !== '') { + setClearSearchDisable(false) + } + + BucketFunctions.getAllBuckets(page, searchTerm) + .then((res) => { + setAllBuckets(res) + }) + .catch((e) => { + console.log(e) + }) + } + + const onClearResultClick = () => { + setPage(1) + setSearchTerm('') + setClearSearchDisable(true) + } + + const getMoreBuckets = () => { + setPage(page + 1) + } + + return ( + +
+
+

My Buckets

+ +
+
+ {myBuckets.map((bucket) => ( + + ))} +
+
+

Search for trending buckets

+
+ setSearchTerm(e.target.value)} + placeholder="Search..." + className="w-1/3" + /> +
+ + +
+
+
+ {allBuckets?.buckets.map((bucket) => )} +
+
+ +
+
+
+
+ ) +} diff --git a/src/app/services/bucket.service.ts b/src/app/services/bucket.service.ts new file mode 100644 index 00000000..9c24b7e6 --- /dev/null +++ b/src/app/services/bucket.service.ts @@ -0,0 +1,216 @@ +import type { ShareBucketProps } from '@/components/buckets/ShareBucketModal' +import fetchWithAuth from '@/utils/fetchWithAuth' + +const getAllBuckets = async (page: number, name?: string) => { + try { + const url = name ? `/api/bucket?page=1&pageSize=10&name=${name}` : `/api/bucket?page=${page}&pageSize=10` + const res = await fetchWithAuth(url, { + method: 'GET', + cache: 'no-cache' + }) + if (!res.ok) { + console.log('Response status:', res.status) + throw new Error('HTTP response was not OK') + } + const json = await res.json() + return json + } catch (error) { + console.log('An error has occurred: ', error) + } +} + +const createBucket = async (body: { title: string; description?: string; isPublic: boolean }) => { + try { + const res = await fetchWithAuth('/api/bucket', { + method: 'POST', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }) + if (!res.ok) { + console.log('Response status:', res.status) + throw new Error('HTTP response was not OK') + } + const json = await res.json() + return json + } catch (error) { + console.log('An error has occurred: ', error) + } +} + +const deleteBucket = async (id: number) => { + try { + const res = await fetchWithAuth(`/api/bucket/${id}`, { + method: 'DELETE', + cache: 'no-cache' + }) + if (!res.ok) { + console.log('Response status:', res.status) + throw new Error('HTTP response was not OK') + } + const json = await res.json() + return json + } catch (error) { + console.log('An error has occurred: ', error) + } +} + +const addCompaniesToBucket = async (bucketId: number, selectedCompanies: string[]) => { + try { + const res = await fetchWithAuth( + `/api/companyBucketRelation?bucketId=${bucketId}&companyId=${+selectedCompanies[0]}`, + { + method: 'POST', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + } + } + ) + if (!res.ok) { + console.log('Response status:', res.status) + throw new Error('HTTP response was not OK') + } + const json = await res.json() + return json + } catch (error) { + console.log('An error has occurred: ', error) + } +} + +const getAllCompanies = async () => { + try { + const res = await fetchWithAuth('/api/company', { + method: 'GET', + cache: 'no-cache' + }) + if (!res.ok) { + console.log('Response status:', res.status) + throw new Error('HTTP response was not OK') + } + const json = await res.json() + return json + } catch (error) { + console.log('An error has occurred: ', error) + } +} + +const getBucketById = async (id: number) => { + try { + const res = await fetchWithAuth(`/api/bucket/${id}`, { + method: 'GET', + cache: 'no-cache' + }) + if (!res.ok) { + console.log('Response status:', res.status) + throw new Error('HTTP response was not OK') + } + const json = await res.json() + return json + } catch (e) {} +} + +const getCompaniesForBucket = async (bucketId: number) => { + try { + const res = await fetchWithAuth(`/api/companyBucketRelation?bucketId=${bucketId}`, { + method: 'GET', + cache: 'no-cache' + }) + if (!res.ok) { + console.log('Response status:', res.status) + throw new Error('HTTP response was not OK') + } + const json = await res.json() + return json + } catch (e) { + return e + } +} + +const updateBucket = async (title: string, description: string | null, id: number, isPublic: boolean) => { + try { + const res = await fetchWithAuth(`/api/bucket/${id}`, { + method: 'PUT', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ title, description, isPublic }) + }) + if (!res.ok) { + console.log('Response status:', res.status) + throw new Error('HTTP response was not OK') + } + const json = await res.json() + return json + } catch (e) {} +} + +const getUsersForBucketAccess = async () => { + try { + const res = await fetchWithAuth(`/api/user`, { + method: 'GET', + cache: 'no-cache' + }) + if (!res.ok) { + console.log('Response status:', res.status) + throw new Error('HTTP response was not OK') + } + const json = await res.json() + return json + } catch (e) {} +} + +const shareBucket = async (body: ShareBucketProps) => { + try { + const res = await fetchWithAuth(`/api/bucket/share?bucketId=${body.bucketId}`, { + method: 'POST', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }) + if (!res.ok) { + console.log('Response status:', res.status) + throw new Error('HTTP response was not OK') + } + const json = await res.json() + return json + } catch (error) { + console.log('An error has occurred: ', error) + } +} + +const getInvitees = async (bucketId: number) => { + try { + const res = await fetchWithAuth(`/api/bucket/share/${bucketId}`, { + method: 'GET', + cache: 'no-cache' + }) + if (!res.ok) { + console.log('Response status:', res.status) + throw new Error('HTTP response was not OK') + } + const json = await res.json() + return json + } catch (error) { + console.log('An error has occurred: ', error) + } +} + +export default { + createBucket, + deleteBucket, + addCompaniesToBucket, + getAllCompanies, + getBucketById, + getCompaniesForBucket, + updateBucket, + getUsersForBucketAccess, + shareBucket, + getInvitees, + getAllBuckets +} diff --git a/src/components/GoBackButton.tsx b/src/components/GoBackButton.tsx new file mode 100644 index 00000000..9170b3bc --- /dev/null +++ b/src/components/GoBackButton.tsx @@ -0,0 +1,14 @@ +import { ArrowLeftIcon } from '@heroicons/react/20/solid' +import Link from 'next/link' + +export const GoBackButton = ({ url }: { url: string }) => { + return ( +
+ +
+ +
+ +
+ ) +} diff --git a/src/components/Popup.tsx b/src/components/Popup.tsx new file mode 100644 index 00000000..2917decb --- /dev/null +++ b/src/components/Popup.tsx @@ -0,0 +1,13 @@ +import { Callout } from '@tremor/react' +import { CheckCircleIcon } from '@heroicons/react/20/solid' +import { PopupType } from '@/types/popup' + +export const Popup = ({ text, title, popupType }: { text: string; title: string; popupType: PopupType }) => { + return ( +
+ + {text} + +
+ ) +} diff --git a/src/components/buckets/DeleteBucketModal.tsx b/src/components/buckets/DeleteBucketModal.tsx new file mode 100644 index 00000000..836d40a4 --- /dev/null +++ b/src/components/buckets/DeleteBucketModal.tsx @@ -0,0 +1,51 @@ +'use client' + +import { Button } from '@tremor/react' + +interface DeleteBucketModalProps { + handleClose: () => void + handleDelete: () => void +} + +const DeleteBucketModal: React.FC = ({ handleClose, handleDelete }) => { + return ( +
+ {/* Background overlay */} + + {/* Modal */} +
+ {/* Close button */} + +
+

Delete this bucket

+
+

+ Are you sure you want to delete the bucket? This will permanently remove it and this cannot be undone. +

+
+
+
+
+ + +
+
+
+
+ ) +} +export default DeleteBucketModal diff --git a/src/components/buckets/EditBucketModal.tsx b/src/components/buckets/EditBucketModal.tsx new file mode 100644 index 00000000..6be6ab09 --- /dev/null +++ b/src/components/buckets/EditBucketModal.tsx @@ -0,0 +1,90 @@ +'use client' + +import { useState } from 'react' +import { Button, Switch } from '@tremor/react' +import { FormContent } from '../FormContent' + +interface EditBucketModalProps { + handleClose: () => void + title: string + description: string | null + isPublic: boolean + handleSave: (title: string, description: string | null, isPublic: boolean) => void +} + +const EditBucketModal: React.FC = ({ + handleClose, + title: titleProp, + description: descriptionProp, + isPublic: isPublicProp, + handleSave +}) => { + const [title, setTitle] = useState(titleProp) + const [description, setDescription] = useState(descriptionProp) + const [isPublic, setIsPublic] = useState(isPublicProp) + + const handleSaveClick = () => { + handleSave(title, description, isPublic) + handleClose() + } + return ( +
+ {/* Background overlay */} + + {/* Modal */} +
+ {/* Close button */} + + +
+

Edit bucket

+
+ setTitle(e.target.value)} + /> +
+
+ setDescription(e.target.value)} + /> +
+
+
Is public
+ setIsPublic(val)} /> +
+ +
+ + +
+
+
+
+ ) +} +export default EditBucketModal diff --git a/src/components/buckets/ShareBucketModal.tsx b/src/components/buckets/ShareBucketModal.tsx new file mode 100644 index 00000000..fbcdef32 --- /dev/null +++ b/src/components/buckets/ShareBucketModal.tsx @@ -0,0 +1,176 @@ +'use client' + +import { Button, SearchSelect, SearchSelectItem, Select, SelectItem } from '@tremor/react' +import { useEffect, useState } from 'react' +import { $Enums, type User } from '@prisma/client' +import BucketFunctions from '@/app/services/bucket.service' + +interface ShareBucketModalProps { + handleClose: () => void + handleShare: (shareUsersList: ShareBucketProps[]) => void + id: string +} + +export interface ShareBucketProps { + bucketId: number + inviteeId: number + permission: string +} + +interface Invitee { + bucketId: number + createdAt: Date + modifiedAt: Date + inviteeId: number + permission: string + user: User +} +const initialValue: User[] = [ + { + id: 0, + authId: '', + name: '', + profilePicture: null, + role: $Enums.Role.USER, + createdAt: new Date(), + modifiedAt: new Date() + } +] + +const ShareBucketModal: React.FC = ({ handleClose, handleShare, id }) => { + const [users, setUsers] = useState(initialValue) + const [usersToShare, setUsersToShare] = useState([]) + const [listToPost, setListToPost] = useState([]) + const [invitees, setInvitees] = useState([]) + + useEffect(() => { + BucketFunctions.getUsersForBucketAccess() + .then((res: User[]) => setUsers(res)) + .catch((err) => console.log(err)) + + BucketFunctions.getInvitees(+id) + .then((res) => setInvitees(res)) + .catch((err) => console.log(err)) + }, [id]) + + const addUserToShareList = (userId: string) => { + const user = users.find((user) => user.id === +userId) + if (user) { + const uniqueArray = [...usersToShare, user].filter( + (function () { + const seenIds = new Set() + return function (object) { + if (seenIds.has(object.id)) { + return false + } else { + seenIds.add(object.id) + return true + } + } + })() + ) + setUsersToShare(uniqueArray) + } + } + + const prepareUserListToShare = (user: User, permission: string) => { + setListToPost((prev) => [ + ...prev, + { + bucketId: +id, + inviteeId: user.id, + permission + } + ]) + } + + const onShareBucket = () => { + handleShare(listToPost) + } + return ( +
+ {/* Background overlay */} + + {/* Modal */} +
+ {/* Close button */} + +
+

Share this bucket

+
+

+ Only private buckets can be shared with others. Please make the bucket private if you want to share it + with other people. People can already search public buckets +

+
+
+

Search for emails to whom you want to share this bucket with

+ +
+ addUserToShareList(val)}> + {users?.map((user) => ( + + {user.name} + + ))} + +
+ +
+ {usersToShare.map((user) => ( +
+ {user.name} + +
+ ))} + + {invitees.length > 0 && ( +
+

Invitees

+ {invitees.map((invitee) => ( +
+ {invitee.user.name} +

{invitee.permission}

+
+ ))} +
+ )} +
+
+
+
+
+ + +
+
+
+
+ ) +} +export default ShareBucketModal diff --git a/src/components/buckets/bucketCard.tsx b/src/components/buckets/bucketCard.tsx new file mode 100644 index 00000000..96b6e8af --- /dev/null +++ b/src/components/buckets/bucketCard.tsx @@ -0,0 +1,20 @@ +'use client' +import type { Bucket } from '@prisma/client' +import { Button } from '@tremor/react' +import Link from 'next/link' + +export default function BucketCard({ bucket }: { bucket: Bucket }) { + return ( +
+

{bucket.title}

+

{bucket.description}

+
+
+ +
+
+
+ ) +} diff --git a/src/types/popup.ts b/src/types/popup.ts new file mode 100644 index 00000000..7d253225 --- /dev/null +++ b/src/types/popup.ts @@ -0,0 +1,4 @@ +export enum PopupType { + SUCCESS = 'SUCCESS', + ERROR = 'ERROR' +} diff --git a/src/utils/fetchWithAuth.ts b/src/utils/fetchWithAuth.ts new file mode 100644 index 00000000..d85a6178 --- /dev/null +++ b/src/utils/fetchWithAuth.ts @@ -0,0 +1,35 @@ +import { getAuth } from 'firebase/auth' + +function getCurrentUserToken(): Promise { + return new Promise((resolve, reject) => { + const unsubscribe = getAuth().onAuthStateChanged(async (user) => { + unsubscribe() // Detach the observer + + if (user) { + try { + const token = await user.getIdToken() + resolve(token) + } catch (error) { + reject(new Error(`Failed to get ID token: ${error}`)) + } + } else { + reject(new Error('No user logged in')) + } + }, reject) + }) +} +const fetchWithAuth = async (url: string, options?: RequestInit) => { + const token = await getCurrentUserToken() + + // Add the Authorization header to the request + const headers = new Headers(options?.headers || {}) + headers.append('Authorization', `${token}`) + + // Return the fetch promise + return await fetch(url, { + ...options, + headers + }) +} + +export default fetchWithAuth