464 lines
15 KiB
TypeScript
Executable File
464 lines
15 KiB
TypeScript
Executable File
"use client";
|
||
|
||
import React, { useRef, useState, useEffect, useLayoutEffect } from "react";
|
||
|
||
import { Upload } from "antd";
|
||
import { Col, Row } from "antd/lib/grid";
|
||
|
||
import "../css/App.scss"
|
||
import "../css/base/spacing.scss"
|
||
import "../css/base/modal.scss"
|
||
import "../css/base/tabs.scss"
|
||
import "../css/base/text.scss"
|
||
import "../css/base/display.scss"
|
||
import "../css/base/form.scss"
|
||
import "../css/base/button.scss"
|
||
import "../css/onboardingFlow.scss"
|
||
import "../css/LinkEditorModal.scss";
|
||
import "../css/ProductEditorModal.scss";
|
||
import "../css/ModuleOndemandVideo.scss";
|
||
import "../css/ModuleShopify.scss"
|
||
|
||
|
||
import Tabs from "antd/lib/tabs";
|
||
import Button from "antd/lib/button";
|
||
import Alert from "antd/lib/alert";
|
||
import * as yup from "yup";
|
||
import classNames from "classnames";
|
||
import { Field, Form, Formik, FormikProps } from "formik";
|
||
|
||
import Modal from "@/components/Modal/Modal"; // Adjust this path as needed
|
||
import { Text } from "@/components/Typography/Text";
|
||
import ImageUpload from "@/components/ImageUpload/ImageUpload";
|
||
import { AntInput, FieldType } from "@/components/Form/FormItem";
|
||
import { LinkItem } from "@/models/talent/talent-profile-module.model";
|
||
|
||
//import icon
|
||
import InfoIcon from "@/icons/Infoicon";
|
||
import CloseOutlineIcon from "@/icons/Closeoutlineicon";
|
||
|
||
const initialProductValues: LinkItem = {
|
||
title: "",
|
||
url: "",
|
||
order: 0,
|
||
visible: true,
|
||
};
|
||
|
||
const initialSpecialOfferValues: LinkItem = {
|
||
title: "",
|
||
url: "",
|
||
order: 0,
|
||
specialOffer: {
|
||
thumbnail: "",
|
||
title: "",
|
||
storeUrl: "",
|
||
couponCode: "",
|
||
},
|
||
visible: true,
|
||
};
|
||
const LINK_TYPES = {
|
||
NORMAL: "NORMAL",
|
||
SPECIAL_OFFER: "SPECIAL_OFFER",
|
||
};
|
||
|
||
const Icon = ({ className, name, width, height }: { className?: string; name: string; width?: number; height?: number }) => {
|
||
return (
|
||
<span className={className} style={{ width, height }}>
|
||
[icon: {name}]
|
||
</span>
|
||
);
|
||
};
|
||
|
||
const dummyPhoto = {
|
||
url: "https://via.placeholder.com/369", // static test image
|
||
fileName: "placeholder.jpg",
|
||
loading: false,
|
||
};
|
||
|
||
const onUploadSuccess = (file: any) => {
|
||
console.log("Upload success:", file);
|
||
};
|
||
|
||
const onRemove = (file: any) => {
|
||
console.log("Removed:", file);
|
||
};
|
||
|
||
const validateLinkSchema = yup.object().shape({
|
||
title: yup.string().required("Please enter the link title"),
|
||
url: yup
|
||
.string()
|
||
.required("Please enter the URL")
|
||
.url("Please enter a valid URL"),
|
||
});
|
||
|
||
const validateSpecialOfferSchema = yup.object().shape({
|
||
specialOffer: yup.object().shape({
|
||
title: yup
|
||
.string()
|
||
.required("Please add the offer information")
|
||
.test("len", "Offer not more than 50 characters", (val) => {
|
||
if (val == undefined) {
|
||
return true;
|
||
}
|
||
return val.length == 0 || (val.length >= 1 && val.length <= 50);
|
||
}),
|
||
storeUrl: yup
|
||
.string()
|
||
.required("Please enter the URL")
|
||
.url("Please enter a valid URL"),
|
||
}),
|
||
});
|
||
|
||
export type LinkEditorModalFormValues = Pick<
|
||
LinkItem,
|
||
"thumbnail" | "url" | "title" | "specialOffer" | "order" | "visible"
|
||
>;
|
||
|
||
|
||
export default function Page() {
|
||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||
|
||
// Dummy state vars to eliminate TS errors
|
||
const [linkType, setLinkType] = useState<string>(LINK_TYPES.NORMAL);
|
||
const [isEdit, setIsEdit] = useState(false);
|
||
const [showAlert, setShowAlert] = useState(true);
|
||
|
||
const [initialValues, setInitialValues] = useState<LinkEditorModalFormValues>();
|
||
|
||
function handleSubmit(values: LinkEditorModalFormValues, formikHelpers: FormikHelpers<LinkEditorModalFormValues>): void | Promise<any> {
|
||
throw new Error("Function not implemented.");
|
||
}
|
||
|
||
const dummyOptions = [
|
||
{ label: "Option A", value: "A" },
|
||
{ label: "Option B", value: "B" },
|
||
];
|
||
|
||
const [modalTop, setModalTop] = useState<number>(); // default safe gap
|
||
const modalRef = useRef<HTMLDivElement | null>(null);
|
||
let newTop: number;
|
||
|
||
//CUSTOM LOGIC: HANDLE THE TOP OF THE MODAL OF DIFFERENT PLATFORMS
|
||
useEffect(() => {
|
||
const updateModalTop = () => {
|
||
const modalEl = document.querySelector('.link-editor-modal') as HTMLElement;
|
||
if (!modalEl) return;
|
||
|
||
const modalHeight = modalEl.getBoundingClientRect().height;
|
||
const windowHeight = window.innerHeight;
|
||
|
||
const isMobile = window.innerWidth <= 768;
|
||
|
||
//PLATFORM LOGIC
|
||
if(!isMobile) {
|
||
if (modalHeight + 60 > windowHeight) {
|
||
newTop = 20;
|
||
setModalTop(newTop);
|
||
//console.log('DESKTOP OVERFLOW');
|
||
|
||
} else {
|
||
//newTop = Math.max((windowHeight - modalHeight) / 4, 30);
|
||
newTop = 20;
|
||
setModalTop(newTop);
|
||
//console.log('DESKTOP CENTERED');
|
||
|
||
}
|
||
}
|
||
else {
|
||
if (modalHeight + 60 > windowHeight) {
|
||
newTop = 20;
|
||
setModalTop(newTop);
|
||
//console.log('MOBILE OVERFLOW');
|
||
} else {
|
||
//newTop = 20;
|
||
//setModalTop(newTop);
|
||
newTop = Math.max( (windowHeight / 2) - (2.4 * modalHeight), 30);
|
||
setModalTop(newTop);
|
||
//modalEl.style.removeProperty('top');
|
||
//console.log('MOBILE CENTERED');
|
||
}
|
||
}
|
||
|
||
modalEl.style.top = `${newTop}px`; // 🔥 Direct DOM update
|
||
|
||
};
|
||
|
||
if (isModalOpen) {
|
||
setTimeout(updateModalTop, 100); // Wait for DOM to fully render
|
||
window.addEventListener("resize", updateModalTop);
|
||
}
|
||
|
||
return () => {
|
||
window.removeEventListener("resize", updateModalTop);
|
||
};
|
||
}, [isModalOpen, linkType]);
|
||
|
||
|
||
useLayoutEffect(() => {
|
||
const adjustMargin = () => {
|
||
if (!isModalOpen) return;
|
||
|
||
if (linkType === LINK_TYPES.SPECIAL_OFFER) {
|
||
//console.log('SPECIAL')
|
||
const form = document.querySelector(".offer-form-item") as HTMLElement;
|
||
const inputBox = form.getElementsByClassName('ant-form-item-has-success')[0] as HTMLElement;
|
||
const hasError = form.querySelector(".ant-form-item-explain-error") as HTMLElement;
|
||
|
||
|
||
if (inputBox) {
|
||
inputBox.style.marginBottom = hasError ? "24px" : "0px";
|
||
}
|
||
}
|
||
|
||
if (linkType === LINK_TYPES.NORMAL) {
|
||
//console.log('NORMAL')
|
||
const form = document.querySelector(".link-editor-form") as HTMLElement;
|
||
const inputBoxes = form?.getElementsByClassName("ant-form-item-has-success");
|
||
const inputBox = inputBoxes?.[1] as HTMLElement;
|
||
|
||
if (inputBox) {
|
||
inputBox.style.marginBottom = "24px";
|
||
}
|
||
}
|
||
};
|
||
|
||
// Delay just until after DOM mounts, but before paint
|
||
requestAnimationFrame(() => {
|
||
adjustMargin();
|
||
});
|
||
|
||
}, [linkType, isModalOpen]);
|
||
|
||
//DEALING WITH ERROR LABELS
|
||
const [errorActivated, setErrorActivated] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (!isModalOpen) return;
|
||
|
||
const formRoot = document.querySelector(".offer-form-item");
|
||
if (!formRoot) return;
|
||
|
||
const observer = new MutationObserver(() => {
|
||
const errorLabels = formRoot.querySelectorAll(".ant-form-item-explain-error");
|
||
|
||
// ✅ You can react to the presence of ANY error
|
||
if (errorLabels.length > 0) {
|
||
//console.log("🚨 Error label activated");
|
||
// Optional: do something here
|
||
setErrorActivated(true)
|
||
} else {
|
||
console.log("✅ No errors");
|
||
}
|
||
});
|
||
|
||
observer.observe(formRoot, {
|
||
childList: true,
|
||
subtree: true,
|
||
});
|
||
|
||
return () => observer.disconnect();
|
||
}, [isModalOpen, linkType]);
|
||
|
||
|
||
//TRIGGER FOR ACTIVATED ERROR
|
||
useEffect(() => {
|
||
|
||
if(errorActivated) {
|
||
console.log('ERROR ACTIVATED');
|
||
const form = document.querySelector(".offer-form-item") as HTMLElement;
|
||
const inputBox = form.getElementsByClassName('ant-form-item')[0] as HTMLElement;
|
||
|
||
|
||
if (inputBox) {
|
||
inputBox.style.marginBottom = "24px";
|
||
}
|
||
}
|
||
|
||
}, [errorActivated]);
|
||
|
||
|
||
|
||
return (
|
||
<>
|
||
{/** INLINE ROOT STYLING */}
|
||
<style
|
||
data-rc-order="append"
|
||
rc-util-key="-ant-1753973781591-0.9765637550520021-dynamic-theme"
|
||
dangerouslySetInnerHTML={{
|
||
__html:
|
||
"\n :root {\n --ant-primary-color: rgb(18, 18, 18);\n--ant-primary-color-disabled: #454545;\n--ant-primary-color-hover: #1f1f1f;\n--ant-primary-color-active: #000000;\n--ant-primary-color-outline: rgba(18, 18, 18, 0.2);\n--ant-primary-color-deprecated-bg: #454545;\n--ant-primary-color-deprecated-border: #2b2b2b;\n--ant-primary-1: #525252;\n--ant-primary-2: #454545;\n--ant-primary-3: #383838;\n--ant-primary-4: #2b2b2b;\n--ant-primary-5: #1f1f1f;\n--ant-primary-6: #121212;\n--ant-primary-7: #000000;\n--ant-primary-8: #000000;\n--ant-primary-9: #000000;\n--ant-primary-10: #000000;\n--ant-primary-color-deprecated-l-35: rgb(107, 107, 107);\n--ant-primary-color-deprecated-l-20: rgb(69, 69, 69);\n--ant-primary-color-deprecated-t-20: rgb(65, 65, 65);\n--ant-primary-color-deprecated-t-50: rgb(137, 137, 137);\n--ant-primary-color-deprecated-f-12: rgba(18, 18, 18, 0.12);\n--ant-primary-color-active-deprecated-f-30: rgba(82, 82, 82, 0.3);\n--ant-primary-color-active-deprecated-d-02: rgb(77, 77, 77);\n--ant-success-color: rgba(255, 255, 255, 0.1);\n--ant-success-color-disabled: #ffffff;\n--ant-success-color-hover: #ffffff;\n--ant-success-color-active: #b3b3b3;\n--ant-success-color-outline: rgba(255, 255, 255, 0.2);\n--ant-success-color-deprecated-bg: #ffffff;\n--ant-success-color-deprecated-border: #ffffff;\n--ant-info-color: rgb(255, 255, 255);\n--ant-info-color-disabled: #ffffff;\n--ant-info-color-hover: #ffffff;\n--ant-info-color-active: #b3b3b3;\n--ant-info-color-outline: rgba(255, 255, 255, 0.2);\n--ant-info-color-deprecated-bg: #ffffff;\n--ant-info-color-deprecated-border: #ffffff;\n }\n "
|
||
}}
|
||
/>
|
||
|
||
|
||
<p>1: <a href="http://localhost:3000">Home</a></p>
|
||
|
||
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
|
||
|
||
<Modal
|
||
open={isModalOpen}
|
||
width={664}
|
||
closable
|
||
style={{
|
||
marginLeft: "auto",
|
||
marginRight: "auto",
|
||
top: modalTop,
|
||
}}
|
||
className="link-editor-modal m__t--24"
|
||
title="Add New Link"
|
||
onCancel={() => setIsModalOpen(false)}
|
||
maskClosable={false}
|
||
ref={modalRef}
|
||
>
|
||
<Tabs
|
||
onChange={(activeKey) => setLinkType(activeKey)}
|
||
activeKey={linkType}
|
||
className={isEdit ? "is-edit" : ""}
|
||
>
|
||
{!isEdit && (
|
||
<Tabs.TabPane tab={<Text>Link</Text>} key={LINK_TYPES.NORMAL} />
|
||
)}
|
||
{!isEdit && (
|
||
<Tabs.TabPane
|
||
tab={<Text>Special offers</Text>}
|
||
key={LINK_TYPES.SPECIAL_OFFER}
|
||
>
|
||
{showAlert && (
|
||
<Alert
|
||
className={classNames(
|
||
"m__t--24 p__x--8 p__y--8 bg--darkGray shopify-module__alert"
|
||
)}
|
||
description={
|
||
"You can add special offer links and discount codes. When a customer clicks on the link, they will be presented with a popup where they’ll be able to copy the discount code (if applicable) and directed to the website."
|
||
}
|
||
icon={<InfoIcon width={24} height={24} />}
|
||
showIcon
|
||
closeText={
|
||
<CloseOutlineIcon width={24} height={24} name="close-outline" />
|
||
}
|
||
onClose={() => {
|
||
setShowAlert(false);
|
||
localStorage.setItem(
|
||
"CLOSE_ALERT_ADD_SPECIAL_OFFER",
|
||
"true"
|
||
);
|
||
}}
|
||
/>
|
||
)}
|
||
</Tabs.TabPane>
|
||
)}
|
||
</Tabs>
|
||
|
||
<div>
|
||
{/* IMAGE UPLOAD BUTTON */}
|
||
<div className={isEdit ? "" : "m__y--32"}>
|
||
<Row align="middle" justify="center">
|
||
<Col>
|
||
<ImageUpload
|
||
className="product-editor__cover m__b--20"
|
||
photo={dummyPhoto}
|
||
onUploadSuccess={onUploadSuccess}
|
||
onRemove={onRemove}
|
||
dropAspect={1}
|
||
/>
|
||
</Col>
|
||
<Col className="m__b--4" span={24}>
|
||
<Text className="d--block text__align--center" preset="semibold18">
|
||
Thumbnail Photo
|
||
</Text>
|
||
</Col>
|
||
<Col span={24}>
|
||
<Text
|
||
className="d--block text__align--center text--grey4"
|
||
preset="regular14"
|
||
>
|
||
{/* //TODO */}
|
||
Use a size that’s at least 369 x 369 pixels and 6MB or less
|
||
</Text>
|
||
</Col>
|
||
</Row>
|
||
</div>
|
||
|
||
<Formik
|
||
initialValues={initialValues as LinkEditorModalFormValues}
|
||
onSubmit={handleSubmit}
|
||
validationSchema={
|
||
linkType === LINK_TYPES.NORMAL
|
||
? validateLinkSchema
|
||
: validateSpecialOfferSchema
|
||
}
|
||
>
|
||
<>
|
||
<Form
|
||
className="link-editor-form"
|
||
>
|
||
{linkType === LINK_TYPES.NORMAL ? (
|
||
<>
|
||
<Field
|
||
component={AntInput}
|
||
name="url"
|
||
type={FieldType.input}
|
||
label="URL"
|
||
placeholder="https://example.com"
|
||
/>
|
||
<Field
|
||
component={AntInput}
|
||
name="title"
|
||
type={FieldType.input}
|
||
label="Title"
|
||
placeholder="Enter title of the link"
|
||
/>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Field
|
||
component={AntInput}
|
||
name="specialOffer.storeUrl"
|
||
type={FieldType.input}
|
||
label="URL"
|
||
placeholder="https://example.com"
|
||
className="m__b--20"
|
||
/>
|
||
<Field
|
||
component={AntInput}
|
||
name="specialOffer.title"
|
||
type={FieldType.input}
|
||
label="What is the offer?"
|
||
placeholder="Add the offer information"
|
||
className="offer-form-item"
|
||
/>
|
||
<Text
|
||
id="discountLabel" className="d--block m__t--8 m__b--20 opacity--60" preset="regular14">{`0/50 characters`}</Text>
|
||
<Field
|
||
component={AntInput}
|
||
name="specialOffer.couponCode"
|
||
type={FieldType.input}
|
||
label="Discount code (optional)"
|
||
placeholder="Add the discount code"
|
||
/>
|
||
</>
|
||
)}
|
||
</Form>
|
||
<div className="link-editor__actions m__t--16">
|
||
<Button
|
||
className="ant-btn-xl ant-btn-uppercase"
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
className="ant-btn-xl ant-btn-uppercase"
|
||
type="primary"
|
||
>
|
||
Done
|
||
</Button>
|
||
</div>
|
||
</>
|
||
</Formik>
|
||
|
||
</div>
|
||
|
||
</Modal>
|
||
</>
|
||
);
|
||
}
|