diff --git a/component_factory/package.json b/component_factory/package.json
index 19ac513..bf413a2 100755
--- a/component_factory/package.json
+++ b/component_factory/package.json
@@ -13,6 +13,7 @@
"formik": "^2.4.6",
"next": "15.4.5",
"react": "19.1.0",
+ "react-beautiful-dnd": "^13.1.1",
"react-dom": "19.1.0",
"redux": "^5.0.1",
"sass": "^1.89.2",
diff --git a/component_factory/src/app/simple_modal/page.tsx b/component_factory/src/app/LinkEditorModal/page.tsx
old mode 100644
new mode 100755
similarity index 100%
rename from component_factory/src/app/simple_modal/page.tsx
rename to component_factory/src/app/LinkEditorModal/page.tsx
diff --git a/component_factory/src/app/ShopifyProductCard/mockData.ts b/component_factory/src/app/ShopifyProductCard/mockData.ts
new file mode 100644
index 0000000..e8a75da
--- /dev/null
+++ b/component_factory/src/app/ShopifyProductCard/mockData.ts
@@ -0,0 +1,20 @@
+// File: InterfaceFactory/app/ShopifyProductCard/mockData.ts
+
+export interface ShopifyProduct {
+ id: string;
+ title: string;
+ description: string;
+ image: string;
+ price: number;
+ available: boolean;
+ }
+
+ export const mockProduct: ShopifyProduct = {
+ id: 'prod_1',
+ title: 'Mock Shopify T-Shirt',
+ description: 'A stylish t-shirt made from 100% organic cotton. Perfect for developers.',
+ image: '/mock-images/shopify-tshirt.png', // You may need to add this image or use a placeholder URL
+ price: 29.99,
+ available: true,
+ };
+
\ No newline at end of file
diff --git a/component_factory/src/app/ShopifyProductCard/page.tsx b/component_factory/src/app/ShopifyProductCard/page.tsx
new file mode 100644
index 0000000..01b0277
--- /dev/null
+++ b/component_factory/src/app/ShopifyProductCard/page.tsx
@@ -0,0 +1,49 @@
+// File: InterfaceFactory/app/ShopifyProductCard/page.tsx
+'use client';
+
+import React, { useEffect } from 'react';
+import { ConfigProvider, App as AntdApp } from 'antd';
+import enUS from 'antd/es/locale/en_US';
+import ShopifyProductCard from '@/components/ShopifyProductCard/ShopifyProductCard';
+import '@/app/css/ShopifyProductCard.scss';
+
+import { mockProduct, ShopifyProduct } from './mockData';
+
+// Minimal shape the card actually uses
+type MinimalShopifyProductItem = {
+ id?: string;
+ title: string;
+ images: { src: string }[];
+ variants: { price: string }[];
+ available?: boolean;
+};
+
+const toShopifyProductItem = (p: ShopifyProduct): MinimalShopifyProductItem => ({
+ id: p.id,
+ title: p.title,
+ images: [{ src: p.image }], // ✅ card expects images[0].src
+ variants: [{ price: String(p.price) }],// ✅ card expects variants[0].price (string)
+ available: p.available,
+});
+
+export default function ShopifyProductCardPage() {
+ // If the card uses a portal, ensure it exists (safe no-op otherwise)
+ useEffect(() => {
+ if (!document.getElementById('root-portal')) {
+ const el = document.createElement('div');
+ el.id = 'root-portal';
+ document.body.appendChild(el);
+ }
+ }, []);
+
+ return (
+
+
+
+ {/* ✅ Provide the correct prop shape */}
+
+
+
+
+ );
+}
diff --git a/component_factory/src/app/css/FanClubCard.scss b/component_factory/src/app/css/FanClubCard.scss
new file mode 100644
index 0000000..271122d
--- /dev/null
+++ b/component_factory/src/app/css/FanClubCard.scss
@@ -0,0 +1,36 @@
+@import './variables.scss';
+
+.fan-club-card {
+ &__title {
+ width: 100%;
+ max-width: 298px;
+ }
+ &__divider {
+ width: 100%;
+ height: 1px;
+ background: $gray5;
+ max-width: 252px;
+ }
+ &__cover {
+ width: 166px;
+ height: 166px;
+ border-radius: 50% !important;
+ margin-bottom: 0;
+
+ @include mobile {
+ width: 112px;
+ height: 112px;
+ }
+ .ant-upload {
+ border-radius: 50%;
+ }
+ img {
+ border-radius: 50% !important;
+ }
+ }
+ .btn-delete {
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
+}
diff --git a/component_factory/src/app/css/ImageSkeleton.scss b/component_factory/src/app/css/ImageSkeleton.scss
new file mode 100644
index 0000000..b390bf4
--- /dev/null
+++ b/component_factory/src/app/css/ImageSkeleton.scss
@@ -0,0 +1,15 @@
+.image-skeleton {
+ img {
+ object-fit: cover;
+ //object-position: top;
+ }
+ .avatar-profile__text {
+ font-weight: 500;
+ }
+ &--circle {
+ img {
+ border-radius: 100% !important;
+ }
+ }
+ }
+
\ No newline at end of file
diff --git a/component_factory/src/app/css/ModuleEditor.scss b/component_factory/src/app/css/ModuleEditor.scss
new file mode 100644
index 0000000..e39e2f7
--- /dev/null
+++ b/component_factory/src/app/css/ModuleEditor.scss
@@ -0,0 +1,135 @@
+@import './variables.scss';
+
+@media (max-width: 768px) {
+ .module-editor__title--count {
+ position: absolute;
+ top: 24px;
+ margin-left: 0;
+ }
+
+ .module-editor__title--input {
+ font-size: 18px;
+ font-weight: 600;
+ height: 36px;
+ width: 100%;
+ }
+}
+
+.module-editor {
+ &__title {
+ min-width: 380px;
+ position: relative;
+ }
+
+ &__title--count {
+ position: absolute;
+ left: 0;
+ top: 28px;
+ color: $dark !important;
+ opacity: 0, 5;
+ margin-left: 65px;
+ }
+
+ &__title--input {
+ font-size: 20px;
+ font-weight: 500;
+ padding: 0;
+ caret-color: $default !important;
+ }
+
+ &__title--label {
+ line-height: 32px !important;
+ }
+
+ &__acitons {
+ border-radius: 16px;
+ border-right: none;
+ -webkit-border-radius: 16px;
+ -moz-border-radius: 16px;
+ -webkit-box-shadow: $boxShadow2;
+ -moz-box-shadow: $boxShadow2;
+ box-shadow: $boxShadow2;
+
+ .ant-list-header {
+ width: 212px;
+ height: 46px;
+ padding: 11px 20px;
+ }
+
+ .ant-list-item {
+ width: 212px;
+ height: 56px;
+ padding: 16px 20px;
+ cursor: pointer;
+
+ &:hover {
+ background-color: rgba(37, 127, 252, 0.05);
+ }
+
+ &:first-child {
+ border-radius: 16px 16px 0 0;
+ }
+
+ &:last-child {
+ border-radius: 0 0 16px 16px;
+ border-end-end-radius: 16px !important;
+ }
+ }
+ }
+
+ &__wrapper--with-bottom-actions {
+ .pannel-collapse .ant-collapse-item {
+ border-radius: 16px 16px 0 0 !important;
+ }
+ }
+}
+.module-editor-overlay {
+ padding-top: 0;
+ margin-top: -4px;
+ .ant-popover-inner {
+ border-radius: 8px;
+ }
+
+ .ant-popover-inner-content {
+ border-radius: 8px;
+ padding: 0;
+ }
+}
+.ant-select-dropdown {
+ z-index: 99999;
+}
+.ant-popover-arrow {
+ display: none;
+}
+
+.module-title-row {
+ flex-shrink: 1;
+ max-width: 100%;
+ min-width: 100px;
+
+ button {
+ width: 100%;
+
+ div {
+ width: 100%;
+
+ &.ant-typography {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+}
+
+.module-editor-bottom-actions {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ border-radius: 0 0 16px 16px;
+ padding: 12px 20px;
+ background: var(--crt-sys-color-background-surface-lowest-default);
+
+ > button {
+ width: 100%;
+ }
+}
diff --git a/component_factory/src/app/css/PopoverMenu.scss b/component_factory/src/app/css/PopoverMenu.scss
new file mode 100644
index 0000000..3c91e43
--- /dev/null
+++ b/component_factory/src/app/css/PopoverMenu.scss
@@ -0,0 +1,55 @@
+@import 'variables.scss';
+
+.popover-menu-actions {
+ border-radius: 16px;
+ border-right: none;
+ -webkit-border-radius: 16px;
+ -moz-border-radius: 16px;
+ -webkit-box-shadow: $boxShadow2;
+ -moz-box-shadow: $boxShadow2;
+ box-shadow: $boxShadow2;
+
+ .ant-list-item {
+ width: 204px;
+ height: 46px;
+ padding: 12px;
+ cursor: pointer;
+
+ &:hover {
+ background-color: rgba(37, 127, 252, 0.05);
+ }
+
+ &:first-child {
+ border-radius: 16px 16px 0 0;
+ }
+
+ &:last-child {
+ border-radius: 0 0 16px 16px !important;
+ }
+ &.title {
+ padding-bottom: 8px;
+ border-bottom: none;
+ &:hover {
+ background-color: transparent;
+ }
+ }
+ }
+}
+.popover-menu-overlay {
+ padding-top: 0;
+ margin-top: -4px;
+ .ant-popover-inner {
+ border-radius: 16px;
+ }
+
+ .ant-popover-inner-content {
+ border-radius: 16px;
+ padding: 0;
+ }
+}
+.ant-select-dropdown {
+ z-index: 99999;
+}
+.ant-popover-arrow {
+ display: none;
+}
diff --git a/component_factory/src/app/css/Progress.scss b/component_factory/src/app/css/Progress.scss
new file mode 100644
index 0000000..f9960ed
--- /dev/null
+++ b/component_factory/src/app/css/Progress.scss
@@ -0,0 +1,24 @@
+@import './variables.scss';
+
+.progress {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ .ant-progress {
+ &-inner {
+ width: 32px !important;
+ height: 32px !important;
+
+ svg path {
+ stroke-width: 10px !important;
+ }
+ }
+ }
+
+ .ant-typography {
+ font-size: 10px !important;
+ color: rgba($default, 0.5);
+ }
+}
diff --git a/component_factory/src/app/css/ShopifyProductCard.scss b/component_factory/src/app/css/ShopifyProductCard.scss
new file mode 100644
index 0000000..a49ea0f
--- /dev/null
+++ b/component_factory/src/app/css/ShopifyProductCard.scss
@@ -0,0 +1,62 @@
+@import './variables';
+
+.on-demand-delete-confirm-modal {
+ overflow: hidden;
+}
+
+.shopify-product-card {
+ overflow: hidden;
+ .price {
+ color: $grey4;
+ }
+
+ &__picture {
+ img,
+ .ant-skeleton-avatar {
+ width: 82px !important;
+ height: 82px !important;
+ object-fit: cover;
+ border-right: 1px solid $grey2;
+ }
+ }
+
+ &__info {
+ padding: 0 16px;
+ width: 334px;
+ &--content {
+ .ondemand-drag-btn {
+ padding-right: 0;
+ }
+
+ &__status {
+ font-size: 12px;
+ line-height: 20px;
+ padding: 0 6px;
+ color: #ffffff;
+ display: inline-block;
+ border-radius: 4px;
+
+ &--active {
+ background-color: $green;
+ }
+
+ &--draft {
+ background-color: $gray8;
+ }
+
+ &--unpublished {
+ background-color: $yellow;
+ }
+ }
+ }
+ }
+ &__title {
+ max-width: 224px;
+ }
+ &__empty {
+ width: 82px;
+ height: 82px;
+ background-color: $gray2;
+ border-right: 1px solid $grey2;
+ }
+}
diff --git a/component_factory/src/components/ImageSkeleton/ImageSkeleton.tsx b/component_factory/src/components/ImageSkeleton/ImageSkeleton.tsx
new file mode 100644
index 0000000..bc2af22
--- /dev/null
+++ b/component_factory/src/components/ImageSkeleton/ImageSkeleton.tsx
@@ -0,0 +1,69 @@
+import { Skeleton, Avatar } from 'antd'
+import React, { useEffect, useState } from 'react'
+import '@/app/css/ImageSkeleton.scss'
+
+interface IProps {
+ src: string | undefined
+ imageClassName?: string
+ skeletonClassName?: string
+ shape?: 'square' | 'circle'
+ size?: number | 'small' | 'large' | 'default' | undefined
+ imgWidth?: string | number
+ name?: string
+}
+
+const ImageSkeleton = ({
+ src,
+ imageClassName = '',
+ skeletonClassName = '',
+ shape = 'square',
+ size = 'large',
+ imgWidth = '24',
+ name
+}: IProps) => {
+ const [isLoaded, setLoaded] = useState(false)
+ const [isError, setError] = useState(false)
+ const imgEl: any = React.useRef(null)
+
+ useEffect(() => {
+ if (imgEl.current?.complete) {
+ setLoaded(true)
+ }
+ }, [])
+
+ return (
+
+ {src && (
+
setLoaded(true)}
+ onError={() => {
+ setLoaded(false)
+ setError(true)
+ }}
+ style={{
+ width: `${imgWidth}px`,
+ height: `${imgWidth}px`
+ }}
+ />
+ )}
+ {isError && name ? (
+
+ NAME
+
+ ) : (
+
+ )}
+
+ )
+}
+
+export default ImageSkeleton
diff --git a/component_factory/src/components/PopoverMenu/ModuleEditor.tsx b/component_factory/src/components/PopoverMenu/ModuleEditor.tsx
new file mode 100644
index 0000000..1d44c0d
--- /dev/null
+++ b/component_factory/src/components/PopoverMenu/ModuleEditor.tsx
@@ -0,0 +1,100 @@
+import React, { useCallback } from "react";
+import { Button, List, Popover } from "antd";
+import { Paragraph } from "@/components/Typography/Paragraph";
+import { TypographyPresets } from "@/components/Typography/Preset";
+import "@/app/css/ModuleEditor.scss"; // use styles from the original source file
+
+export type PopoverMenuItem = {
+ key: string;
+ label: string;
+ onClick?: () => void;
+ className?: string;
+ itemClassName?: string;
+ labelPreset?: TypographyPresets;
+};
+
+interface ModuleEditorProps {
+ children: React.ReactNode;
+ data?: PopoverMenuItem[];
+ title?: string;
+ visible?: boolean; // controlled visibility (v4: visible, v5: open)
+ onChangeVisible?: (value: boolean) => void; // setter for visibility
+}
+
+const ModuleEditor = ({
+ children,
+ data = [],
+ title,
+ visible,
+ onChangeVisible,
+}: ModuleEditorProps) => {
+ const handleClickItem = useCallback(
+ (item: PopoverMenuItem) => () => {
+ // mirror original: close first, then execute the action
+ onChangeVisible?.(false);
+ item.onClick?.();
+ },
+ [onChangeVisible]
+ );
+
+ const renderItems = () =>
+ data.map((item) => (
+
+
+ {item.label}
+
+
+ ));
+
+ // Support both antd v4 and v5 props (harmless on either)
+ const popProps: any = {
+ placement: "bottomRight", // original placement
+ overlayClassName: "module-editor-overlay", // original overlay class
+ trigger: ["click"], // original trigger
+ arrow: false, // v5 way to hide arrow
+ arrowContent: null, // v4 compatibility (noop on v5)
+ content: (
+
+ {title && (
+
+
+ {title}
+
+
+ )}
+ {renderItems()}
+
+ ),
+ // controlled visibility
+ visible, // v4
+ onVisibleChange: onChangeVisible,
+ open: visible, // v5
+ onOpenChange: onChangeVisible,
+ };
+
+ return (
+
+ {
+ // keep scope-local toggle; match original stopPropagation behavior
+ e.stopPropagation();
+ onChangeVisible?.(true);
+ }}
+ >
+ {children}
+
+
+ );
+};
+
+export default ModuleEditor;
diff --git a/component_factory/src/components/PopoverMenu/PopoverMenu.tsx b/component_factory/src/components/PopoverMenu/PopoverMenu.tsx
new file mode 100644
index 0000000..d3d6e12
--- /dev/null
+++ b/component_factory/src/components/PopoverMenu/PopoverMenu.tsx
@@ -0,0 +1,89 @@
+import { Button, List, Popover } from "antd";
+import { Paragraph } from "@/components/Typography/Paragraph";
+import { TypographyPresets } from "@/components/Typography/Preset";
+import * as React from "react";
+import { useCallback } from "react";
+import "@/app/css/PopoverMenu.scss";
+
+export type PopoverMenuItem = {
+ key: string;
+ label: string;
+ onClick?: () => void;
+ className?: string;
+ itemClassName?: string;
+ labelPreset?: TypographyPresets;
+};
+
+interface PopoverMenuProps {
+ children: React.ReactNode;
+ data?: PopoverMenuItem[];
+ title?: string;
+ visible?: boolean;
+ onChangeVisible?: (value: boolean) => void;
+}
+
+const PopoverMenu = ({
+ children,
+ data = [],
+ title,
+ visible,
+ onChangeVisible,
+}: PopoverMenuProps) => {
+ const handleClickItem = useCallback(
+ (item) => () => {
+ onChangeVisible?.(false);
+ item.onClick?.();
+ },
+ [data]
+ );
+
+ const renderItems = () => {
+ return data.map((item) => (
+
+
+ {item.label}
+
+
+ ));
+ };
+ return (
+
+ {title && (
+
+
+ {title}
+
+
+ )}
+ {renderItems()}
+
+ }
+ trigger={["click"]}
+ onVisibleChange={onChangeVisible}
+ >
+ onChangeVisible?.(true)}
+ >
+ {children}
+
+
+ );
+};
+
+export default PopoverMenu;
diff --git a/component_factory/src/components/ShopifyProductCard/ShopifyProductCard.tsx b/component_factory/src/components/ShopifyProductCard/ShopifyProductCard.tsx
new file mode 100644
index 0000000..c289722
--- /dev/null
+++ b/component_factory/src/components/ShopifyProductCard/ShopifyProductCard.tsx
@@ -0,0 +1,92 @@
+
+import React, { useState } from 'react'
+import { Button, Col, Row } from 'antd'
+import ImageSkeleton from '@/components/ImageSkeleton/ImageSkeleton'
+import { Text } from '@/components/Typography/Text'
+import { ShopifyProductItem } from '@/models/talent/talent-profile-module.model'
+
+interface Props {
+ index?: number
+ item: ShopifyProductItem
+ showPrice?: boolean
+ disableDragDrop?: boolean
+ type: number
+ onEdit?: (item: ShopifyProductItem) => void
+ onDelete?: (item: ShopifyProductItem) => void
+ active?: boolean
+}
+
+const PRODUCT_TYPE_INDIVIDUAL = 1
+
+const ShopifyProductCard: React.FC = ({
+ item,
+ showPrice = true,
+ disableDragDrop = false,
+ type,
+ onDelete
+}) => {
+ const variant = item?.variants?.reduce(function (prev, curr) {
+ return parseFloat(prev.price) < parseFloat(curr.price) ? prev : curr
+ })
+
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
+
+ const onDeleteConfirm = () => {
+ typeof onDelete === 'function' && onDelete(item)
+ setShowDeleteConfirm(false)
+ }
+
+ return (
+
+ {item && (
+
+
+ {item.images?.[0]?.src ? (
+
+ ) : (
+
+
+ )}
+
+
+
+
+ {item.title}
+ {showPrice && (
+
+
+
+
+ )}
+
+
+
+
+ setShowDeleteConfirm(true)}
+ >
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export default ShopifyProductCard
diff --git a/interface_components/Component_ProgressBar/page.tsx b/interface_components/Component_ProgressBar/page.tsx
new file mode 100644
index 0000000..8a8775a
--- /dev/null
+++ b/interface_components/Component_ProgressBar/page.tsx
@@ -0,0 +1,28 @@
+'use client'
+
+import React from 'react'
+import { Progress as ProgressAntd, Typography } from 'antd'
+import '@/app/css/Progress.scss'
+
+const { Text } = Typography
+
+interface IProps {
+ value: number | undefined
+}
+
+const Progress = ({ value }: IProps) => {
+ return (
+
+
+
{`${value || 15}%`}
+
+ )
+}
+
+export default Progress
diff --git a/interface_components_final/LinkEditorModal/page.tsx b/interface_components_final/LinkEditorModal/page.tsx
new file mode 100755
index 0000000..8a02eea
--- /dev/null
+++ b/interface_components_final/LinkEditorModal/page.tsx
@@ -0,0 +1,463 @@
+"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 (
+
+ [icon: {name}]
+
+ );
+};
+
+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(LINK_TYPES.NORMAL);
+ const [isEdit, setIsEdit] = useState(false);
+ const [showAlert, setShowAlert] = useState(true);
+
+ const [initialValues, setInitialValues] = useState();
+
+ function handleSubmit(values: LinkEditorModalFormValues, formikHelpers: FormikHelpers): void | Promise {
+ throw new Error("Function not implemented.");
+ }
+
+ const dummyOptions = [
+ { label: "Option A", value: "A" },
+ { label: "Option B", value: "B" },
+ ];
+
+ const [modalTop, setModalTop] = useState(); // default safe gap
+ const modalRef = useRef(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 */}
+
+
+
+ 1: Home
+
+ setIsModalOpen(true)}>Open Modal
+
+ setIsModalOpen(false)}
+ maskClosable={false}
+ ref={modalRef}
+ >
+ setLinkType(activeKey)}
+ activeKey={linkType}
+ className={isEdit ? "is-edit" : ""}
+ >
+ {!isEdit && (
+ Link} key={LINK_TYPES.NORMAL} />
+ )}
+ {!isEdit && (
+ Special offers}
+ key={LINK_TYPES.SPECIAL_OFFER}
+ >
+ {showAlert && (
+ }
+ showIcon
+ closeText={
+
+ }
+ onClose={() => {
+ setShowAlert(false);
+ localStorage.setItem(
+ "CLOSE_ALERT_ADD_SPECIAL_OFFER",
+ "true"
+ );
+ }}
+ />
+ )}
+
+ )}
+
+
+
+ {/* IMAGE UPLOAD BUTTON */}
+
+
+
+
+
+
+
+ Thumbnail Photo
+
+
+
+
+ {/* //TODO */}
+ Use a size that’s at least 369 x 369 pixels and 6MB or less
+
+
+
+
+
+
+ <>
+
+
+
+ Cancel
+
+
+ Done
+
+
+ >
+
+
+
+
+
+ >
+ );
+}
diff --git a/interface_factory_pages/EmptyModule/page.tsx b/interface_factory_pages/EmptyModule/page.tsx
new file mode 100644
index 0000000..97509f2
--- /dev/null
+++ b/interface_factory_pages/EmptyModule/page.tsx
@@ -0,0 +1,95 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import { Row, Col } from 'antd/lib/grid';
+import Button from 'antd/lib/button';
+import { Paragraph } from '@/components/Typography/Paragraph';
+import { Text } from '@/components/Typography/Text';
+
+// ⛔ Mock useWindowSize (since actual hook isn't available)
+function useWindowSize() {
+ const [width, setWidth] = useState(undefined);
+
+ useEffect(() => {
+ function handleResize() {
+ setWidth(window.innerWidth);
+ }
+
+ handleResize(); // Set initial
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ return { width };
+}
+
+// ✅ Simulated store (mocked useProfileStore)
+const useProfileStore = () => {
+ const [modules, setModules] = useState([]);
+ const addModule = () => {
+ const newModule = {
+ id: Date.now(),
+ title: 'New Module',
+ description: 'Describe your module here',
+ };
+ setModules([...modules, newModule]);
+ };
+
+ return { modules, addModule };
+};
+
+export default function EmptyModulePage() {
+ const { modules, addModule } = useProfileStore();
+ const { width } = useWindowSize();
+ const isMobileStyleChangesEnabled = width !== undefined && width <= 768;
+
+ return (
+
+ {modules.length === 0 ? (
+
+
+
+
+
+ Nothing Here Yet
+
+
+ Click the button below to get started
+
+
+
+
+
+
+ Add Module
+
+
+
+
+
+ ) : (
+
+
Modules:
+
+ {modules.map((mod) => (
+
+ {mod.title} - {mod.description}
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/interface_factory_pages/fanclubcard/page.tsx b/interface_factory_pages/fanclubcard/page.tsx
new file mode 100644
index 0000000..db61b75
--- /dev/null
+++ b/interface_factory_pages/fanclubcard/page.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { Col, Row } from 'antd'
+import classNames from 'classnames'
+import * as React from 'react'
+import { useCallback, useEffect, useState } from 'react'
+import { Paragraph } from '@/components/Typography/Paragraph'
+
+import '../css/FanClubCard.scss'
+
+interface FanClubCardProps {
+ title?: string
+ subTitle?: string
+ image?: string
+ onSaveFanClub?: (data: any) => void
+}
+
+const FanClubCard = ({
+ title,
+ subTitle,
+ image,
+ onSaveFanClub
+}: FanClubCardProps) => {
+ const [photo, setPhoto] = useState({})
+
+ useEffect(() => {
+ setPhoto({ url: image })
+ }, [])
+ const uploadPhotoSuccess = useCallback(
+ (file: MediaUpload) => {
+ setPhoto(file)
+ const data = {
+ order: 0,
+ thumbnail: file.url
+ }
+ onSaveFanClub?.(data)
+ },
+ [onSaveFanClub]
+ )
+ const removePhoto = useCallback(() => {
+ setPhoto({})
+ const data = {
+ order: 0,
+ thumbnail: ''
+ }
+ onSaveFanClub?.(data)
+ }, [onSaveFanClub])
+ return (
+
+
+
+
+ {title}
+
+
+ {subTitle}
+
+
+
+ HELLO
+
+
+
+ )
+}
+
+export default FanClubCard
diff --git a/interface_factory_pages/popover/page.tsx b/interface_factory_pages/popover/page.tsx
new file mode 100644
index 0000000..2cdbe9e
--- /dev/null
+++ b/interface_factory_pages/popover/page.tsx
@@ -0,0 +1,41 @@
+"use client";
+import React, { useState } from "react";
+import PopoverMenu, { PopoverMenuItem } from "@/components/PopoverMenu/ModuleEditor";
+import { Button } from "antd";
+
+export default function Page() {
+ const [visible, setVisible] = useState(false);
+
+ const menuItems: PopoverMenuItem[] = [
+ {
+ key: "1",
+ label: "Profile",
+ onClick: () => alert("Profile clicked"),
+ },
+ {
+ key: "2",
+ label: "Settings",
+ onClick: () => alert("Settings clicked"),
+ },
+ {
+ key: "3",
+ label: "Logout",
+ onClick: () => alert("Logout clicked"),
+ className: "text-red-500",
+ },
+ ];
+
+ return (
+
+
Testing PopoverMenu
+
+ Open Menu
+
+
+ );
+}
diff --git a/interface_factory_pages/scratchbuild/page.tsx b/interface_factory_pages/scratchbuild/page.tsx
new file mode 100644
index 0000000..ef95a86
--- /dev/null
+++ b/interface_factory_pages/scratchbuild/page.tsx
@@ -0,0 +1,59 @@
+'use client'
+
+import React from 'react'
+import { Button, Col, Row } from 'antd'
+import { Text } from '@/components/Typography/Text'
+
+export interface IProps {
+ isReloading: boolean
+ close: () => void
+ reload: () => void
+}
+
+const empty = () => ({
+ title: 'Select Product Type',
+ content: (
+ <>
+
+
+
+
+
+ No Products Found
+
+
+ You have no products in your Shopify Store. To add products to your
+ Komi profile, you need to add them to your Shopify store first. You
+ can click reload once your products are added to refresh this page.
+
+
+
+ >
+ ),
+ footer: (
+
+
+
+ Close
+
+
+
+
+ Reload
+
+
+
+ )
+})
+
+export default empty
diff --git a/interface_factory_pages/simple_modal/page.tsx b/interface_factory_pages/simple_modal/page.tsx
new file mode 100644
index 0000000..8a02eea
--- /dev/null
+++ b/interface_factory_pages/simple_modal/page.tsx
@@ -0,0 +1,463 @@
+"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 (
+
+ [icon: {name}]
+
+ );
+};
+
+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(LINK_TYPES.NORMAL);
+ const [isEdit, setIsEdit] = useState(false);
+ const [showAlert, setShowAlert] = useState(true);
+
+ const [initialValues, setInitialValues] = useState();
+
+ function handleSubmit(values: LinkEditorModalFormValues, formikHelpers: FormikHelpers): void | Promise {
+ throw new Error("Function not implemented.");
+ }
+
+ const dummyOptions = [
+ { label: "Option A", value: "A" },
+ { label: "Option B", value: "B" },
+ ];
+
+ const [modalTop, setModalTop] = useState(); // default safe gap
+ const modalRef = useRef(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 */}
+
+
+
+ 1: Home
+
+ setIsModalOpen(true)}>Open Modal
+
+ setIsModalOpen(false)}
+ maskClosable={false}
+ ref={modalRef}
+ >
+ setLinkType(activeKey)}
+ activeKey={linkType}
+ className={isEdit ? "is-edit" : ""}
+ >
+ {!isEdit && (
+ Link} key={LINK_TYPES.NORMAL} />
+ )}
+ {!isEdit && (
+ Special offers}
+ key={LINK_TYPES.SPECIAL_OFFER}
+ >
+ {showAlert && (
+ }
+ showIcon
+ closeText={
+
+ }
+ onClose={() => {
+ setShowAlert(false);
+ localStorage.setItem(
+ "CLOSE_ALERT_ADD_SPECIAL_OFFER",
+ "true"
+ );
+ }}
+ />
+ )}
+
+ )}
+
+
+
+ {/* IMAGE UPLOAD BUTTON */}
+
+
+
+
+
+
+
+ Thumbnail Photo
+
+
+
+
+ {/* //TODO */}
+ Use a size that’s at least 369 x 369 pixels and 6MB or less
+
+
+
+
+
+
+ <>
+
+
+
+ Cancel
+
+
+ Done
+
+
+ >
+
+
+
+
+
+ >
+ );
+}
diff --git a/component_factory/src/app/special_offer_modal/page.tsx b/interface_factory_pages/special_offer_modal/page.tsx
similarity index 100%
rename from component_factory/src/app/special_offer_modal/page.tsx
rename to interface_factory_pages/special_offer_modal/page.tsx