diff --git a/package-lock.json b/package-lock.json index 1b45a1d4..57e2f5d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@lexical/react": "^0.12.2", "@nanostores/persistent": "^0.9.1", "@nanostores/solid": "^0.4.2", + "@neodrag/solid": "^2.0.4", "@playwright/test": "^1.41.2", "@stripe/stripe-js": "^3.0.6", "@supabase/supabase-js": "^2.39.6", @@ -4065,6 +4066,14 @@ "solid-js": "^1.6.0" } }, + "node_modules/@neodrag/solid": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@neodrag/solid/-/solid-2.0.4.tgz", + "integrity": "sha512-iTt2CaN1mEhvy7fMalizYWPzwUmt8FI6OUnNNTwjumn8I04uyfNMrBYOq8PMrf5QssYUL+LRWCrh7UQ1WOZ9+w==", + "peerDependencies": { + "solid-js": "^1.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index 916c39bd..df44f188 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@lexical/react": "^0.12.2", "@nanostores/persistent": "^0.9.1", "@nanostores/solid": "^0.4.2", + "@neodrag/solid": "^2.0.4", "@playwright/test": "^1.41.2", "@stripe/stripe-js": "^3.0.6", "@supabase/supabase-js": "^2.39.6", diff --git a/src/components/common/notices/modal.tsx b/src/components/common/notices/modal.tsx index 6427ab56..8d86492f 100644 --- a/src/components/common/notices/modal.tsx +++ b/src/components/common/notices/modal.tsx @@ -10,6 +10,7 @@ type ModalProps = { buttonContent: JSX.Element; heading: HeadingProps["heading"]; headingLevel?: HeadingProps["headingLevel"]; + classList?: string; }; const Modal: Component = (props) => { @@ -70,6 +71,7 @@ const Modal: Component = (props) => { e.stopPropagation(); setIsOpen(false); } + return ( <> + + + + {/* */} {/* NOTE: Quantity and AddToCart styles updated/correct in mobile merge */}
diff --git a/src/components/posts/MobileFullPostView.tsx b/src/components/posts/MobileFullPostView.tsx index 3de61dd0..9c767f6f 100644 --- a/src/components/posts/MobileFullPostView.tsx +++ b/src/components/posts/MobileFullPostView.tsx @@ -15,6 +15,7 @@ import type { AuthSession } from "@supabase/supabase-js"; import { ReportResource } from "./ReportResource"; import { sortResourceTypes } from "@lib/utils/resourceSort"; import { downloadPostImage, downloadUserImage } from "@lib/imageHelper"; +import { AverageRatingStars } from "./AverageRatingStars"; const lang = getLangFromUrl(new URL(window.location.href)); const t = useTranslations(lang); @@ -635,7 +636,7 @@ export const MobileViewFullPost: Component = (props) => { id="cart-price-div" class="sticky top-0 my-4 flex flex-col bg-background1 dark:bg-background1-DM" > -
+

{t("messages.free")} @@ -646,6 +647,19 @@ export const MobileViewFullPost: Component = (props) => { ${post()?.price.toFixed(2)}

+
+ {post() !== undefined ? ( + + ) : ( +
+ )} +
diff --git a/src/components/posts/ReviewPurchasedResource.tsx b/src/components/posts/ReviewPurchasedResource.tsx new file mode 100644 index 00000000..aed75a7d --- /dev/null +++ b/src/components/posts/ReviewPurchasedResource.tsx @@ -0,0 +1,848 @@ +import Modal from "@components/common/notices/modal"; +import { getLangFromUrl, useTranslations } from "@i18n/utils"; +import type { Review } from "@lib/types"; +import type { Session } from "@supabase/supabase-js"; +import { ReviewSlider } from "./ReviewSlider"; + +import { + createEffect, + createResource, + createSignal, + onMount, + Suspense, + Show, + type Component, +} from "solid-js"; + +interface Props { + resourceId: number; + userId: string; + access: string | undefined; + ref: string; + imgURL: { webpUrl: string; jpegUrl: string } | undefined; + postTitle: string; + postCreator: string; + purchaseDate: string; + createdDate: string; + // review: string; +} + +async function postFormData(formData: FormData) { + const response = await fetch("/api/clientSubmitReviewResource", { + method: "POST", + body: formData, + }); + const data = await response.json(); + if (data.redirect) { + window.location.href = data.redirect; + } + if (response.status === 200) { + console.log("Submitted Review"); + } + return data; +} + +async function fetchPostReviews(resourceId: string) { + const response = await fetch("/api/getAllReviews", { + method: "POST", + body: JSON.stringify({ resource_id: resourceId }), + }); + const data = await response.json(); + if (data.redirect) { + window.location.href = data.redirect; + } + if (response.status === 200) { + console.log(data); + } + return data; +} + +async function fetchUserRating(reviewerId: string, resourceId: number) { + const response = await fetch("/api/getUserRating", { + method: "POST", + body: JSON.stringify({ + reviewer_id: reviewerId, + resource_id: resourceId, + }), + }); + const data = await response.json(); + if (data.redirect) { + window.location.href = data.redirect; + } + if (response.status === 200) { + console.log(data); + } + return data; +} + +//Refactor this should be a prop passed all the way from the Astro page +const lang = getLangFromUrl(new URL(window.location.href)); +const t = useTranslations(lang); + +export const ReviewPurchasedResource: Component = (props) => { + const [overallRating, setOverallRating] = createSignal(""); + const [reviewTitle, setReviewTitle] = createSignal(""); + const [reviewText, setReviewText] = createSignal(""); + const [formData, setFormData] = createSignal(); + const [response] = createResource(formData, postFormData); + const [totalReviews, setTotalReviews] = createSignal(0); + const [reviewsData, setReviewsData] = createSignal([]); + const [loading, setLoading] = createSignal(true); + const [totalRatingOfPost, setTotalRatingOfPost] = createSignal(0); + const [showReviewForm, setShowReviewForm] = createSignal(true); + const [dbReviewNum, setDbReviewNum] = createSignal(0); + + onMount(async () => { + try { + //Refactor: We aren't going to want to load all the reviews every time, probably need pagination + //So we will need to do checks like "has this been reviewed by this user" on the server/API call + const data = await fetchUserRating(props.userId, props.resourceId); + setReviewsData(data.body); + + let reviewerRating = data.body[0].overall_rating; + setDbReviewNum(reviewerRating); + + if (reviewerRating) { + console.log("reviewerRating was true"); + setShowReviewForm(false); + } + } catch (err) { + console.error(err); + } finally { + reviewsData().map((review: Review) => { + if (review.reviewer_id === props.userId) { + return; + } + }); + setLoading(false); + } + + // try { + // //Refactor: We aren't going to want to load all the reviews every time, probably need pagination + // //So we will need to do checks like "has this been reviewed by this user" on the server/API call + // const data = await fetchPostReviews(props.resourceId.toString()); + // setReviewsData(data.body); + // } catch (err) { + // console.error(err); + // } finally { + // const arrayLength = () => reviewsData().length; + // if (arrayLength() === 0) { + // setShowReviewForm(true); + // setLoading(false); + // return; + // } + // reviewsData().map((review: Review) => { + // if (review.reviewer_id === props.userId) { + // return; + // } else { + // setShowReviewForm(true); + // } + // }); + + // // Set loading to false after fetch is complete + // setLoading(false); + // setTotalReviews(arrayLength); + // // Refactor: I would like to see this done on the server as part of the fetch if possible I think it will probably be faster + // // plus we won't want to return every single review but we will need to use them all to calculate this. + // // Might need to use a SQL query of some kind to store the average for the post in the view? Calculating this continually will be slow. + // if (arrayLength() > 0) { + // reviewsData().map((review: Review) => { + // setTotalRatingOfPost( + // review.overall_rating + totalRatingOfPost() + // ); + // }); + // setTotalRatingOfPost(totalRatingOfPost() / totalReviews()); + // setTotalRatingOfPost(Math.round(totalRatingOfPost() * 2) / 2); + // } + // } + }); + + function submit(e: SubmitEvent) { + e.preventDefault(); + + console.log(overallRating(), reviewTitle(), reviewText()); + + const formData = new FormData(e.target as HTMLFormElement); + formData.append("review_title", reviewTitle()); + formData.append("review_text", reviewText()); + formData.append("overall_rating", overallRating()); + formData.append("resource_id", props.resourceId.toString()); + formData.append("user_id", props.userId); + formData.append("refresh_token", props.ref); + formData.append("access_token", props.access ? props.access : ""); + setFormData(formData); + } + + const ratePurchase = (e: Event) => { + let selectedReviewIdEl = e.currentTarget as HTMLSpanElement; + let selectedReviewID = selectedReviewIdEl.id; + + let reviewedDiv = document.getElementById("user-profile-ratings-div"); + + switch (selectedReviewID) { + case "user-rating-1": + setOverallRating("1"); + break; + case "user-rating-2": + setOverallRating("2"); + break; + case "user-rating-3": + setOverallRating("3"); + break; + case "user-rating-4": + setOverallRating("4"); + break; + case "user-rating-5": + setOverallRating("5"); + break; + default: + setOverallRating(""); + } + + reviewedDiv?.setAttribute("id", "user-profile-ratings-div-reviewed"); + }; + + return ( +
+
{loading() &&

Loading reviews...

}
+ 0}> +
{t("postLabels.yourRating")}:
+
+ + + + + + + + + + +
+ } + > + + + +
+ + + + + +
+ } + > + + + + + + + + + +
+ } + > + + + + + + + + + +
+ } + > + + + + +
+ + + + +
+ {props.imgURL?.webpUrl ? ( + + + + ) : ( + + + + )} + +
+

+ {props.postTitle} +

+ +

+ {props.postCreator}Fix Creator Name +

+
+ +
+ +

+ {t("menus.purchased")}  +

+

+ {props.purchaseDate.slice(0, 10)} +

+
+ +
+ + + +

+ {t("menus.updated")}   +

+

+ {props.createdDate.slice(0, 10)} +

+
+
+
+ +
+

+ {t("formLabels.whatDidYouThink")} +

+
+
+
+ * + + {t("formLabels.required")} + +
+ +
+
+
+

+ {t("formLabels.overallRating")}* +

+
+ + + + + + + +
+
+
+ ratePurchase(e)} + > + + ☆ + + + + + + + + + ratePurchase(e)} + > + + ☆ + + + + + + + + + ratePurchase(e)} + > + + ☆ + + + + + + + + + ratePurchase(e)} + > + + ☆ + + + + + + + + + ratePurchase(e)} + > + + ☆ + + + + + + + + +
+
+ {/* + setOverallRating(e.target.value) + } + /> */} +
+ + {/*
*/} + {/*
+
+

{t("formLabels.reviewQ1")}

+ +
+ +
+

{t("formLabels.reviewQ2")}

+ +
+ +
+

{t("formLabels.reviewQ3")}

+ +
+ +
+

{t("formLabels.reviewQ4")}

+ +
+ +
+

{t("formLabels.reviewQ5")}

+ +
+ +
+

{t("formLabels.reviewQ6")}

+ +
+
*/} + +
+
+ +
+ + + + + + + +
+
+
+ + setReviewTitle(e.target.value) + } + /> +
+
+ +
+
+ +
+ + + + + + + +
+
+