From 2799501fa5a3f3765c2af3a4dfee12cbf7821c53 Mon Sep 17 00:00:00 2001 From: Jason Dicker Date: Wed, 24 Apr 2024 14:32:22 +0200 Subject: [PATCH 1/4] initial opportunity sharing popup with preview, copy link, generate QR Code & more options. api changes pending... --- src/web/src/components/Opportunity/Badges.tsx | 102 ++++++ .../opportunities/[opportunityId]/index.tsx | 290 ++++++++++++------ 2 files changed, 290 insertions(+), 102 deletions(-) create mode 100644 src/web/src/components/Opportunity/Badges.tsx diff --git a/src/web/src/components/Opportunity/Badges.tsx b/src/web/src/components/Opportunity/Badges.tsx new file mode 100644 index 000000000..a9a09d95f --- /dev/null +++ b/src/web/src/components/Opportunity/Badges.tsx @@ -0,0 +1,102 @@ +import Image from "next/image"; +import { + IoMdPerson, + IoIosBook, + IoMdPause, + IoMdPlay, + IoMdClose, +} from "react-icons/io"; +import iconClock from "public/images/icon-clock.svg"; +import iconZlto from "public/images/icon-zlto.svg"; +import { useMemo } from "react"; +import { OpportunityInfo } from "~/api/models/opportunity"; + +interface BadgesProps { + opportunity: OpportunityInfo | undefined; // replace 'any' with the actual type +} + +const Badges: React.FC = ({ opportunity }) => { + // memo for spots left i.e participantLimit - participantCountTotal + const spotsLeft = useMemo(() => { + const participantLimit = opportunity?.participantLimit ?? 0; + const participantCountTotal = opportunity?.participantCountTotal ?? 0; + return Math.max(participantLimit - participantCountTotal, 0); + }, [opportunity]); + + return ( +
+ {opportunity?.commitmentIntervalCount && ( +
+ Icon Clock + + {`${ + opportunity.commitmentIntervalCount + } ${opportunity.commitmentInterval}${ + opportunity.commitmentIntervalCount > 1 ? "s" : "" + }`} +
+ )} + {spotsLeft > 0 && ( +
+ + + {spotsLeft} Spots left +
+ )} + {opportunity?.type && ( +
+ + {opportunity.type} +
+ )} + {(opportunity?.zltoReward ?? 0) > 0 && ( +
+ Icon Zlto + {opportunity?.zltoReward} +
+ )} + + {/* STATUS BADGES */} + {opportunity?.status == "Active" && ( + <> + {new Date(opportunity.dateStart) > new Date() && ( +
+ +

Not started

+
+ )} + {new Date(opportunity.dateStart) < new Date() && ( +
+ + Started +
+ )} + + )} + {opportunity?.status == "Expired" && ( +
+ + Expired +
+ )} +
+ ); +}; + +export default Badges; diff --git a/src/web/src/pages/opportunities/[opportunityId]/index.tsx b/src/web/src/pages/opportunities/[opportunityId]/index.tsx index b27fc71ee..ef89ca886 100644 --- a/src/web/src/pages/opportunities/[opportunityId]/index.tsx +++ b/src/web/src/pages/opportunities/[opportunityId]/index.tsx @@ -32,6 +32,7 @@ import { IoMdCalendar, IoMdCloudUpload, } from "react-icons/io"; +import { IoCopy, IoQrCode, IoEllipsisHorizontalOutline } from "react-icons/io5"; import type { NextPageWithLayout } from "~/pages/_app"; import ReactModal from "react-modal"; import iconUpload from "public/images/icon-upload.svg"; @@ -39,7 +40,7 @@ import iconOpen from "public/images/icon-open.svg"; import iconClock from "public/images/icon-clock.svg"; import iconZlto from "public/images/icon-zlto.svg"; import iconBookmark from "public/images/icon-bookmark.svg"; -// import iconShare from "public/images/icon-share.svg"; +import iconShare from "public/images/icon-share.svg"; import iconDifficulty from "public/images/icon-difficulty.svg"; import iconLanguage from "public/images/icon-language.svg"; import iconTopics from "public/images/icon-topics.svg"; @@ -84,6 +85,8 @@ import { AvatarImage } from "~/components/AvatarImage"; import { useRouter } from "next/router"; import { Unauthenticated } from "~/components/Status/Unauthenticated"; import { Unauthorized } from "~/components/Status/Unauthorized"; +import { set } from "nprogress"; +import Badges from "~/components/Opportunity/Badges"; interface IParams extends ParsedUrlQuery { id: string; @@ -162,7 +165,9 @@ const OpportunityDetails: NextPageWithLayout<{ user: User; error?: number; }> = ({ opportunityId, user, error }) => { + const router = useRouter(); const queryClient = useQueryClient(); + const [loginDialogVisible, setLoginDialogVisible] = useState(false); const [gotoOpportunityDialogVisible, setGotoOpportunityDialogVisible] = useState(false); @@ -176,8 +181,9 @@ const OpportunityDetails: NextPageWithLayout<{ ] = useState(false); const [cancelOpportunityDialogVisible, setCancelOpportunityDialogVisible] = useState(false); + const [shareOpportunityDialogVisible, setShareOpportunityDialogVisible] = + useState(false); const [isOppSaved, setIsOppSaved] = useState(false); - const router = useRouter(); const { data: opportunity, @@ -212,13 +218,6 @@ const OpportunityDetails: NextPageWithLayout<{ }, }); - // memo for spots left i.e participantLimit - participantCountTotal - const spotsLeft = useMemo(() => { - const participantLimit = opportunity?.participantLimit ?? 0; - const participantCountTotal = opportunity?.participantCountTotal ?? 0; - return Math.max(participantLimit - participantCountTotal, 0); - }, [opportunity]); - useEffect(() => { if (!user) return; @@ -329,6 +328,26 @@ const OpportunityDetails: NextPageWithLayout<{ setCancelOpportunityDialogVisible(false); }, [opportunityId, queryClient]); + const onShareOpportunity = useCallback(() => { + if (!user) { + toast.warning("You need to be logged in to save an opportunity"); + return; + } + setShareOpportunityDialogVisible(true); + }, [user, setShareOpportunityDialogVisible]); + + const [showQRCode, setShowQRCode] = useState(false); + + const copyToClipboard = () => { + navigator.clipboard.writeText( + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + ); + alert("URL copied to clipboard!"); + }; + const generateQRCode = () => { + setShowQRCode(true); + }; + if (error) { if (error === 401) return ; else if (error === 403) return ; @@ -696,6 +715,155 @@ const OpportunityDetails: NextPageWithLayout<{ + {/* SHARE OPPORTUNITY DIALOG */} + { + setShareOpportunityDialogVisible(false); + }} + className={`fixed bottom-0 left-0 right-0 top-0 flex-grow overflow-hidden bg-white animate-in fade-in md:m-auto md:max-h-[650px] md:w-[600px] md:rounded-3xl`} + portalClassName={"fixed z-40"} + overlayClassName="fixed inset-0 bg-overlay" + > + {/*
*/} +
+
+

+ +
+ +
+
+ Icon Bell +
+ +

Share this opportunity!

+ + {/* OPPORTUNITY DETAILS (smaller) */} +
+
+
+ +
+
+

+ {opportunity?.title} +

+
+ By {opportunity?.organizationName} +
+
+
+ + {/* BADGES */} + + + {/* DATES */} + {/* {opportunity?.status == "Active" && ( +
+
+ {opportunity.dateStart && ( + <> + Starts: + + + {opportunity.dateStart} + + + + )} +
+
+ {opportunity.dateEnd && ( + <> + Ends: + + + {opportunity.dateEnd} + + + + )} +
+
+ )} */} +
+ + {/* BUTTONS */} +
+ + + +
+ + {showQRCode && ( + // QR Code + <>TODO: + )} +
+
+ + {opportunity && (
@@ -720,88 +888,7 @@ const OpportunityDetails: NextPageWithLayout<{ {/* BADGES */} -
-
- Icon Clock - - {`${ - opportunity.commitmentIntervalCount - } ${opportunity.commitmentInterval}${ - opportunity.commitmentIntervalCount > 1 ? "s" : "" - }`} -
- {spotsLeft > 0 && ( -
- - - - {spotsLeft} Spots left - -
- )} - {opportunity?.type && ( -
- - - {opportunity.type} - -
- )} - {(opportunity.zltoReward ?? 0) > 0 && ( -
- Icon Zlto - - {opportunity.zltoReward} - -
- )} - - {/* STATUS BADGES */} - {opportunity?.status == "Active" && ( - <> - {new Date(opportunity.dateStart) > new Date() && ( -
- - - {opportunity.dateStart} - -
- )} - {new Date(opportunity.dateStart) < new Date() && ( -
- - Ongoing -
- )} - - )} - {opportunity?.status == "Expired" && ( -
- - Upload Only -
- )} -
+ {/* DATES */} {opportunity.status == "Active" && ( @@ -973,18 +1060,17 @@ const OpportunityDetails: NextPageWithLayout<{ - {/* */} +
From f2ba174f7ad1d89220a2dd2b415c0f3b328edf99 Mon Sep 17 00:00:00 2001 From: Jason Dicker Date: Thu, 25 Apr 2024 07:43:01 +0200 Subject: [PATCH 2/4] pending api --- src/web/src/components/Opportunity/Share.tsx | 196 ++++++++++++++++++ .../opportunities/[opportunityId]/index.tsx | 191 +++-------------- 2 files changed, 227 insertions(+), 160 deletions(-) create mode 100644 src/web/src/components/Opportunity/Share.tsx diff --git a/src/web/src/components/Opportunity/Share.tsx b/src/web/src/components/Opportunity/Share.tsx new file mode 100644 index 000000000..b750bc5f3 --- /dev/null +++ b/src/web/src/components/Opportunity/Share.tsx @@ -0,0 +1,196 @@ +// SharePopup.tsx +import React, { useState, useRef } from "react"; +import { FaFacebook, FaLinkedin, FaQrcode } from "react-icons/fa"; +import { IoMdClose } from "react-icons/io"; +import { + IoCopy, + IoQrCode, + IoEllipsisHorizontalOutline, + IoShareSocialOutline, +} from "react-icons/io5"; +import { toast } from "react-toastify"; +import { AvatarImage } from "../AvatarImage"; +import Badges from "./Badges"; +import iconBell from "public/images/icon-bell.webp"; +import iconBookmark from "public/images/icon-bookmark.svg"; +import Image from "next/image"; +import { OpportunityInfo } from "~/api/models/opportunity"; +import { DATE_FORMAT_HUMAN } from "~/lib/constants"; +import Moment from "react-moment"; + +interface SharePopupProps { + opportunity: OpportunityInfo; + onClose: () => void; +} + +const SharePopup: React.FC = ({ opportunity, onClose }) => { + const [showQRCode, setShowQRCode] = useState(false); + const copyToClipboard = () => { + navigator.clipboard.writeText( + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + ); + toast("URL copied to clipboard!"); + }; + const generateQRCode = () => { + setShowQRCode(true); + }; + + // const shareOnFacebook = () => { + // window.open( + // `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, + // "_blank", + // ); + // }; + + // const shareOnLinkedIn = () => { + // window.open( + // `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent( + // url, + // )}`, + // "_blank", + // ); + // }; + + return ( +
+
+

+ +
+ +
+
+ {/* Icon Bell */} + +
+ +

Share this opportunity!

+ + {/* OPPORTUNITY DETAILS (smaller) */} +
+
+
+ +
+
+

+ {opportunity?.title} +

+
+ By {opportunity?.organizationName} +
+
+
+ + {/* BADGES */} + + + {/* DATES */} + {opportunity?.status == "Active" && ( +
+
+ {opportunity.dateStart && ( + <> + Starts: + + + {opportunity.dateStart} + + + + )} +
+
+ {opportunity.dateEnd && ( + <> + Ends: + + + {opportunity.dateEnd} + + + + )} +
+
+ )} +
+ + {/* BUTTONS */} +
+ + + +
+ + {showQRCode && ( + // QR Code + QR Code + )} +
+
+ ); +}; + +export default SharePopup; diff --git a/src/web/src/pages/opportunities/[opportunityId]/index.tsx b/src/web/src/pages/opportunities/[opportunityId]/index.tsx index ef89ca886..0e3124d70 100644 --- a/src/web/src/pages/opportunities/[opportunityId]/index.tsx +++ b/src/web/src/pages/opportunities/[opportunityId]/index.tsx @@ -87,6 +87,7 @@ import { Unauthenticated } from "~/components/Status/Unauthenticated"; import { Unauthorized } from "~/components/Status/Unauthorized"; import { set } from "nprogress"; import Badges from "~/components/Opportunity/Badges"; +import Share from "~/components/Opportunity/Share"; interface IParams extends ParsedUrlQuery { id: string; @@ -336,17 +337,17 @@ const OpportunityDetails: NextPageWithLayout<{ setShareOpportunityDialogVisible(true); }, [user, setShareOpportunityDialogVisible]); - const [showQRCode, setShowQRCode] = useState(false); + // const [showQRCode, setShowQRCode] = useState(false); - const copyToClipboard = () => { - navigator.clipboard.writeText( - "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", - ); - alert("URL copied to clipboard!"); - }; - const generateQRCode = () => { - setShowQRCode(true); - }; + // const copyToClipboard = () => { + // navigator.clipboard.writeText( + // "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + // ); + // toast("URL copied to clipboard!"); + // }; + // const generateQRCode = () => { + // setShowQRCode(true); + // }; if (error) { if (error === 401) return ; @@ -716,153 +717,23 @@ const OpportunityDetails: NextPageWithLayout<{
{/* SHARE OPPORTUNITY DIALOG */} - { - setShareOpportunityDialogVisible(false); - }} - className={`fixed bottom-0 left-0 right-0 top-0 flex-grow overflow-hidden bg-white animate-in fade-in md:m-auto md:max-h-[650px] md:w-[600px] md:rounded-3xl`} - portalClassName={"fixed z-40"} - overlayClassName="fixed inset-0 bg-overlay" - > - {/*
*/} -
-
-

- -
- -
-
- Icon Bell -
- -

Share this opportunity!

- - {/* OPPORTUNITY DETAILS (smaller) */} -
-
-
- -
-
-

- {opportunity?.title} -

-
- By {opportunity?.organizationName} -
-
-
- - {/* BADGES */} - - - {/* DATES */} - {/* {opportunity?.status == "Active" && ( -
-
- {opportunity.dateStart && ( - <> - Starts: - - - {opportunity.dateStart} - - - - )} -
-
- {opportunity.dateEnd && ( - <> - Ends: - - - {opportunity.dateEnd} - - - - )} -
-
- )} */} -
- - {/* BUTTONS */} -
- - - -
- - {showQRCode && ( - // QR Code - <>TODO: - )} -
-
- + {opportunity && ( + { + setShareOpportunityDialogVisible(false); + }} + className={`fixed bottom-0 left-0 right-0 top-0 flex-grow overflow-hidden bg-white animate-in fade-in md:m-auto md:max-h-[650px] md:w-[600px] md:rounded-3xl`} + portalClassName={"fixed z-40"} + overlayClassName="fixed inset-0 bg-overlay" + > + setShareOpportunityDialogVisible(false)} + /> + + )} {opportunity && (
@@ -1028,12 +899,12 @@ const OpportunityDetails: NextPageWithLayout<{ )}
- + //TODO: last here, buttons wrapping at md
-
-
- {/* Icon Bell */} - + {/* LOADING */} + {linkInfoIsLoading && ( +
+
+ +
+ )} -

Share this opportunity!

- - {/* OPPORTUNITY DETAILS (smaller) */} -
-
-
- -
-
-

- {opportunity?.title} -

-
- By {opportunity?.organizationName} -
-
+ {/* MAIN CONTENT */} + {!linkInfoIsLoading && ( +
+
+
- {/* BADGES */} - - - {/* DATES */} - {opportunity?.status == "Active" && ( -
-
- {opportunity.dateStart && ( - <> - Starts: - - - {opportunity.dateStart} - - - - )} +

Share this opportunity!

+ + {/* OPPORTUNITY DETAILS (smaller) */} +
+
+
+
-
- {opportunity.dateEnd && ( - <> - Ends: - - - {opportunity.dateEnd} - - - - )} +
+

+ {opportunity?.title} +

+
+ By {opportunity?.organizationName} +
- )} -
- {/* BUTTONS */} -
- - - -
+ {/* BADGES */} + - {showQRCode && ( - // QR Code - QR Code - )} -
+ {/* DATES */} + {opportunity?.status == "Active" && ( +
+
+ {opportunity.dateStart && ( + <> + Starts: + + + {opportunity.dateStart} + + + + )} +
+
+ {opportunity.dateEnd && ( + <> + Ends: + + + {opportunity.dateEnd} + + + + )} +
+
+ )} +
+ + {/* BUTTONS */} +
+ + + +
+ + {/* QR CODE */} + {showQRCode && qrCodeImageData && ( + QR Code + )} +
+ )}
); }; diff --git a/src/web/src/pages/opportunities/[opportunityId]/index.tsx b/src/web/src/pages/opportunities/[opportunityId]/index.tsx index 0e3124d70..a0cc9944d 100644 --- a/src/web/src/pages/opportunities/[opportunityId]/index.tsx +++ b/src/web/src/pages/opportunities/[opportunityId]/index.tsx @@ -6,13 +6,7 @@ import { } from "@tanstack/react-query"; import { type GetServerSidePropsContext } from "next"; import { type ParsedUrlQuery } from "querystring"; -import { - useState, - type ReactElement, - useMemo, - useCallback, - useEffect, -} from "react"; +import { useState, type ReactElement, useCallback, useEffect } from "react"; import { type OpportunityInfo } from "~/api/models/opportunity"; import { getOpportunityInfoById, @@ -25,22 +19,15 @@ import { IoMdClose, IoMdFingerPrint, IoMdArrowRoundBack, - IoMdPlay, IoMdBookmark, - IoMdPerson, - IoIosBook, - IoMdCalendar, - IoMdCloudUpload, + IoMdShare, } from "react-icons/io"; -import { IoCopy, IoQrCode, IoEllipsisHorizontalOutline } from "react-icons/io5"; import type { NextPageWithLayout } from "~/pages/_app"; import ReactModal from "react-modal"; import iconUpload from "public/images/icon-upload.svg"; import iconOpen from "public/images/icon-open.svg"; import iconClock from "public/images/icon-clock.svg"; -import iconZlto from "public/images/icon-zlto.svg"; import iconBookmark from "public/images/icon-bookmark.svg"; -import iconShare from "public/images/icon-share.svg"; import iconDifficulty from "public/images/icon-difficulty.svg"; import iconLanguage from "public/images/icon-language.svg"; import iconTopics from "public/images/icon-topics.svg"; @@ -85,7 +72,6 @@ import { AvatarImage } from "~/components/AvatarImage"; import { useRouter } from "next/router"; import { Unauthenticated } from "~/components/Status/Unauthenticated"; import { Unauthorized } from "~/components/Status/Unauthorized"; -import { set } from "nprogress"; import Badges from "~/components/Opportunity/Badges"; import Share from "~/components/Opportunity/Share"; @@ -523,11 +509,12 @@ const OpportunityDetails: NextPageWithLayout<{ this page upon finishing to{" "} earn your ZLTO.
- {/*
Don’t show me this message again
*/} +
Be mindful of external sites' privacy policy and keep your data private.
+
- //TODO: last here, buttons wrapping at md -
+ +
From 30f71e1b33f6709a1829247cdb9112ecabed82d1 Mon Sep 17 00:00:00 2001 From: Jason Dicker Date: Thu, 25 Apr 2024 11:07:27 +0200 Subject: [PATCH 4/4] local website url fix --- src/api/src/application/Yoma.Core.Api/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/src/application/Yoma.Core.Api/appsettings.json b/src/api/src/application/Yoma.Core.Api/appsettings.json index f28841cda..df111f6e9 100644 --- a/src/api/src/application/Yoma.Core.Api/appsettings.json +++ b/src/api/src/application/Yoma.Core.Api/appsettings.json @@ -27,7 +27,7 @@ }, "AppSettings": { - "AppBaseURL": "http://localhost:3001", + "AppBaseURL": "http://localhost:3000", "Hangfire": { "Username": "admin", "Password": "password"