[FACTORY]: Sampling More Components For Isolation Testing
This commit is contained in:
parent
0301f3a0ab
commit
1acd94923b
@ -13,6 +13,7 @@
|
|||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"next": "15.4.5",
|
"next": "15.4.5",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"sass": "^1.89.2",
|
"sass": "^1.89.2",
|
||||||
|
0
component_factory/src/app/simple_modal/page.tsx → component_factory/src/app/LinkEditorModal/page.tsx
Normal file → Executable file
0
component_factory/src/app/simple_modal/page.tsx → component_factory/src/app/LinkEditorModal/page.tsx
Normal file → Executable file
20
component_factory/src/app/ShopifyProductCard/mockData.ts
Normal file
20
component_factory/src/app/ShopifyProductCard/mockData.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
|
49
component_factory/src/app/ShopifyProductCard/page.tsx
Normal file
49
component_factory/src/app/ShopifyProductCard/page.tsx
Normal file
@ -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 (
|
||||||
|
<ConfigProvider locale={enUS}>
|
||||||
|
<AntdApp>
|
||||||
|
<div style={{ padding: 24, background: '#f9f9f9', minHeight: '100vh' }}>
|
||||||
|
{/* ✅ Provide the correct prop shape */}
|
||||||
|
<ShopifyProductCard item={toShopifyProductItem(mockProduct) as any} type={0} showPrice />
|
||||||
|
</div>
|
||||||
|
</AntdApp>
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
}
|
36
component_factory/src/app/css/FanClubCard.scss
Normal file
36
component_factory/src/app/css/FanClubCard.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
15
component_factory/src/app/css/ImageSkeleton.scss
Normal file
15
component_factory/src/app/css/ImageSkeleton.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
135
component_factory/src/app/css/ModuleEditor.scss
Normal file
135
component_factory/src/app/css/ModuleEditor.scss
Normal file
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
55
component_factory/src/app/css/PopoverMenu.scss
Normal file
55
component_factory/src/app/css/PopoverMenu.scss
Normal file
@ -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;
|
||||||
|
}
|
24
component_factory/src/app/css/Progress.scss
Normal file
24
component_factory/src/app/css/Progress.scss
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
62
component_factory/src/app/css/ShopifyProductCard.scss
Normal file
62
component_factory/src/app/css/ShopifyProductCard.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 (
|
||||||
|
<div className={`image-skeleton image-skeleton--${shape}`}>
|
||||||
|
{src && (
|
||||||
|
<img
|
||||||
|
ref={imgEl}
|
||||||
|
className={isLoaded ? imageClassName : 'd--none'}
|
||||||
|
src={src}
|
||||||
|
alt="avatar"
|
||||||
|
onLoad={() => setLoaded(true)}
|
||||||
|
onError={() => {
|
||||||
|
setLoaded(false)
|
||||||
|
setError(true)
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: `${imgWidth}px`,
|
||||||
|
height: `${imgWidth}px`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isError && name ? (
|
||||||
|
<Avatar className="avatar-profile__text" shape={shape} size={size}>
|
||||||
|
NAME
|
||||||
|
</Avatar>
|
||||||
|
) : (
|
||||||
|
<Skeleton.Avatar
|
||||||
|
className={isLoaded && !!src ? 'd--none' : skeletonClassName}
|
||||||
|
active={!isLoaded && !!src}
|
||||||
|
shape={shape}
|
||||||
|
size={size}
|
||||||
|
></Skeleton.Avatar>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageSkeleton
|
100
component_factory/src/components/PopoverMenu/ModuleEditor.tsx
Normal file
100
component_factory/src/components/PopoverMenu/ModuleEditor.tsx
Normal file
@ -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) => (
|
||||||
|
<List.Item
|
||||||
|
className={item.itemClassName}
|
||||||
|
key={item.key}
|
||||||
|
onClick={handleClickItem(item)}
|
||||||
|
>
|
||||||
|
<Paragraph
|
||||||
|
ellipsis
|
||||||
|
preset={item.labelPreset || "semibold14"}
|
||||||
|
className={item.className}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Paragraph>
|
||||||
|
</List.Item>
|
||||||
|
));
|
||||||
|
|
||||||
|
// 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: (
|
||||||
|
<List className="popover-menu-actions module-editor__acitons">
|
||||||
|
{title && (
|
||||||
|
<List.Item className="title">
|
||||||
|
<Paragraph className="opacity--06" preset="semibold14">
|
||||||
|
{title}
|
||||||
|
</Paragraph>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
{renderItems()}
|
||||||
|
</List>
|
||||||
|
),
|
||||||
|
// controlled visibility
|
||||||
|
visible, // v4
|
||||||
|
onVisibleChange: onChangeVisible,
|
||||||
|
open: visible, // v5
|
||||||
|
onOpenChange: onChangeVisible,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover {...popProps}>
|
||||||
|
<Button
|
||||||
|
className="p--0 d--flex align__items--center height--24 min-h-auto"
|
||||||
|
type="link"
|
||||||
|
onClick={(e) => {
|
||||||
|
// keep scope-local toggle; match original stopPropagation behavior
|
||||||
|
e.stopPropagation();
|
||||||
|
onChangeVisible?.(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModuleEditor;
|
89
component_factory/src/components/PopoverMenu/PopoverMenu.tsx
Normal file
89
component_factory/src/components/PopoverMenu/PopoverMenu.tsx
Normal file
@ -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) => (
|
||||||
|
<List.Item
|
||||||
|
className={item.itemClassName}
|
||||||
|
key={item.key}
|
||||||
|
onClick={handleClickItem(item)}
|
||||||
|
>
|
||||||
|
<Paragraph
|
||||||
|
ellipsis={true}
|
||||||
|
preset={item.labelPreset || "semibold14"}
|
||||||
|
className={item.className}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Paragraph>
|
||||||
|
</List.Item>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
visible={visible}
|
||||||
|
placement="top"
|
||||||
|
overlayClassName="popover-menu-overlay"
|
||||||
|
arrowContent={null}
|
||||||
|
content={
|
||||||
|
<List className="popover-menu-actions ">
|
||||||
|
{title && (
|
||||||
|
<List.Item className="title">
|
||||||
|
<Paragraph className="opacity--06" preset="semibold14">
|
||||||
|
{title}
|
||||||
|
</Paragraph>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
{renderItems()}
|
||||||
|
</List>
|
||||||
|
}
|
||||||
|
trigger={["click"]}
|
||||||
|
onVisibleChange={onChangeVisible}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="p--0 d--flex align__items--center height--24 min-h-auto"
|
||||||
|
type="link"
|
||||||
|
onClick={() => onChangeVisible?.(true)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PopoverMenu;
|
@ -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<Props> = ({
|
||||||
|
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 (
|
||||||
|
<React.Fragment>
|
||||||
|
{item && (
|
||||||
|
<div className="border-box d--flex shopify-product-card border__radius--8">
|
||||||
|
<div className="shopify-product-card__picture">
|
||||||
|
{item.images?.[0]?.src ? (
|
||||||
|
<ImageSkeleton src={item.images?.[0]?.src as string} />
|
||||||
|
) : (
|
||||||
|
<Row
|
||||||
|
align="middle"
|
||||||
|
justify="center"
|
||||||
|
className="shopify-product-card__empty"
|
||||||
|
>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="shopify-product-card__info flex--1 d--flex flex__direction--column p__x--16 p__y--16">
|
||||||
|
<Row align="middle">
|
||||||
|
<Col flex="1">
|
||||||
|
<Text preset="semibold16">{item.title}</Text>
|
||||||
|
{showPrice && (
|
||||||
|
<div className="d--flex align__items--center justify__content--between m__t--4">
|
||||||
|
<Text preset="regular14" className="price">
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
className={
|
||||||
|
type !== PRODUCT_TYPE_INDIVIDUAL ? undefined : 'd--none'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Row wrap={false} gutter={16} align="middle">
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
className="p--0 d--flex align__items--center height--24 min-h-auto"
|
||||||
|
type="link"
|
||||||
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
|
>
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShopifyProductCard
|
28
interface_components/Component_ProgressBar/page.tsx
Normal file
28
interface_components/Component_ProgressBar/page.tsx
Normal file
@ -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 (
|
||||||
|
<div className="progress">
|
||||||
|
<ProgressAntd
|
||||||
|
showInfo={false}
|
||||||
|
type="circle"
|
||||||
|
strokeColor={'#50c32c'}
|
||||||
|
percent={value}
|
||||||
|
status="active"
|
||||||
|
/>
|
||||||
|
<Text className="m__t--4">{`${value || 15}%`}</Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Progress
|
463
interface_components_final/LinkEditorModal/page.tsx
Executable file
463
interface_components_final/LinkEditorModal/page.tsx
Executable file
@ -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 (
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
95
interface_factory_pages/EmptyModule/page.tsx
Normal file
95
interface_factory_pages/EmptyModule/page.tsx
Normal file
@ -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<number | undefined>(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<any[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className={isMobileStyleChangesEnabled ? 'p__t--12' : 'p__t--24'}>
|
||||||
|
{modules.length === 0 ? (
|
||||||
|
<Row
|
||||||
|
data-testid="module--empty"
|
||||||
|
gutter={[10, 10]}
|
||||||
|
className="text__align--center"
|
||||||
|
>
|
||||||
|
<Col span={24}>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<Text
|
||||||
|
data-testid="empty-module--caption"
|
||||||
|
preset="semibold18"
|
||||||
|
className="d--block m__b--4"
|
||||||
|
>
|
||||||
|
Nothing Here Yet
|
||||||
|
</Text>
|
||||||
|
<Paragraph
|
||||||
|
data-testid="empty-module--caption-cta"
|
||||||
|
className="opacity--60 leading-22"
|
||||||
|
>
|
||||||
|
Click the button below to get started
|
||||||
|
</Paragraph>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<Button type="link" onClick={addModule}>
|
||||||
|
<div className="d--flex justify__content--center align__items--center">
|
||||||
|
<Text preset="semibold18" className="text--blue m__l--4">
|
||||||
|
Add Module
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
) : (
|
||||||
|
<div className="p-8 max-w-xl mx-auto space-y-2">
|
||||||
|
<h2 className="text-xl font-bold">Modules:</h2>
|
||||||
|
<ul className="list-disc list-inside">
|
||||||
|
{modules.map((mod) => (
|
||||||
|
<li key={mod.id}>
|
||||||
|
<span className="font-medium">{mod.title}</span> - {mod.description}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
76
interface_factory_pages/fanclubcard/page.tsx
Normal file
76
interface_factory_pages/fanclubcard/page.tsx
Normal file
@ -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<MediaUpload>({})
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="fan-club-card">
|
||||||
|
<Row align="middle">
|
||||||
|
<Col flex={'1'}>
|
||||||
|
<Paragraph
|
||||||
|
preset={'bold32'}
|
||||||
|
className={classNames(
|
||||||
|
'fan-club-card__title m__b--8',
|
||||||
|
'leading-42'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph
|
||||||
|
className="m__b--8 opacity--80 fan-club-card__title"
|
||||||
|
preset={'regular18'}
|
||||||
|
>
|
||||||
|
{subTitle}
|
||||||
|
</Paragraph>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<p>HELLO</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FanClubCard
|
41
interface_factory_pages/popover/page.tsx
Normal file
41
interface_factory_pages/popover/page.tsx
Normal file
@ -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 (
|
||||||
|
<div style={{ padding: "40px" }}>
|
||||||
|
<h2>Testing PopoverMenu</h2>
|
||||||
|
<PopoverMenu
|
||||||
|
title="Menu"
|
||||||
|
data={menuItems}
|
||||||
|
visible={visible}
|
||||||
|
onChangeVisible={setVisible}
|
||||||
|
>
|
||||||
|
<Button type="primary">Open Menu</Button>
|
||||||
|
</PopoverMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
59
interface_factory_pages/scratchbuild/page.tsx
Normal file
59
interface_factory_pages/scratchbuild/page.tsx
Normal file
@ -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: (
|
||||||
|
<>
|
||||||
|
<Row gutter={[10, 10]} className="p__y--24">
|
||||||
|
<Col span={24} className="text__align--center">
|
||||||
|
</Col>
|
||||||
|
<Col span={24} className="text__align--center">
|
||||||
|
<Text preset="semibold18" className="d--block m__b--4">
|
||||||
|
No Products Found
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
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.
|
||||||
|
</Text>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
footer: (
|
||||||
|
<Row justify="end" gutter={[16, 16]}>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
onClick={close}
|
||||||
|
className="text__transform--uppercase letter__spacing--1"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
className="text__transform--uppercase letter__spacing--1"
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default empty
|
463
interface_factory_pages/simple_modal/page.tsx
Normal file
463
interface_factory_pages/simple_modal/page.tsx
Normal file
@ -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 (
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user