From 3e98ad7d5b34b78b4e09f37ef748421f4bd7453e Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Tue, 24 Dec 2024 16:31:59 +0600 Subject: [PATCH 01/18] Membership settings app initialized --- .../membership-settings/components/App.tsx | 45 +++++++++++++ .../components/MembershipSettings.tsx | 63 ++++++++++++++++++ .../entries/pro/membership-settings/index.tsx | 19 ++++++ .../molecules/EmptyState.tsx | 62 +++++++++++++++++ .../services/memberships.ts | 29 ++++++++ .../public/images/membership-empty-state.webp | Bin 0 -> 5958 bytes assets/react/v3/shared/config/styles.ts | 1 + classes/Assets.php | 9 ++- webpack.config.js | 3 +- 9 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 assets/react/v3/entries/pro/membership-settings/components/App.tsx create mode 100644 assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx create mode 100644 assets/react/v3/entries/pro/membership-settings/index.tsx create mode 100644 assets/react/v3/entries/pro/membership-settings/molecules/EmptyState.tsx create mode 100644 assets/react/v3/entries/pro/membership-settings/services/memberships.ts create mode 100644 assets/react/v3/public/images/membership-empty-state.webp diff --git a/assets/react/v3/entries/pro/membership-settings/components/App.tsx b/assets/react/v3/entries/pro/membership-settings/components/App.tsx new file mode 100644 index 0000000000..837ecadce1 --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/components/App.tsx @@ -0,0 +1,45 @@ +import { Global } from '@emotion/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useState } from 'react'; + +import ToastProvider from '@Atoms/Toast'; + +import RTLProvider from '@Components/RTLProvider'; +import { ModalProvider } from '@Components/modals/Modal'; + +import { createGlobalCss } from '@Utils/style-utils'; +import MembershipSettings from './MembershipSettings'; + +function App() { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnWindowFocus: false, + networkMode: 'always', + }, + mutations: { + retry: false, + networkMode: 'always', + }, + }, + }), + ); + + return ( + + + + + + + + + + + ); +} + +export default App; diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx new file mode 100644 index 0000000000..6125748438 --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx @@ -0,0 +1,63 @@ +import { useEffect } from 'react'; +import { FormProvider } from 'react-hook-form'; +import { css } from '@emotion/react'; +import { LoadingSection } from '@Atoms/LoadingSpinner'; +import Show from '@Controls/Show'; +import { spacing } from '@Config/styles'; +import { useFormWithGlobalError } from '@Hooks/useFormWithGlobalError'; +import { type MembershipSettings, useMembershipSettingsQuery } from '../services/memberships'; +import EmptyState from '../molecules/EmptyState'; + +function MembershipSettings() { + const form = useFormWithGlobalError({ + defaultValues: { + memberships: [], + }, + }); + + const { reset } = form; + + const membershipSettingsQuery = useMembershipSettingsQuery(); + + const memberships = membershipSettingsQuery.data?.memberships?.length + ? membershipSettingsQuery.data.memberships + : form.getValues('memberships'); + + const formData = form.watch(); + + useEffect(() => { + if (form.formState.isDirty) { + document.getElementById('save_tutor_option')?.removeAttribute('disabled'); + } + }, [form.formState.isDirty]); + + useEffect(() => { + if (membershipSettingsQuery.data) { + reset(membershipSettingsQuery.data); + } + }, [reset, membershipSettingsQuery.data]); + + if (membershipSettingsQuery.isLoading) { + return ; + } + + return ( +
+ console.log('here!')} />}> + Membership list + + + +
+ ); +} + +export default MembershipSettings; + +const styles = { + wrapper: css` + display: flex; + flex-direction: column; + gap: ${spacing[16]}; + `, +}; diff --git a/assets/react/v3/entries/pro/membership-settings/index.tsx b/assets/react/v3/entries/pro/membership-settings/index.tsx new file mode 100644 index 0000000000..252c76dfd8 --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; + +import ErrorBoundary from '@Components/ErrorBoundary'; +import App from './components/App'; + +const element = document.getElementById('tutor-membership-settings'); +if (element) { + const root = createRoot(element as HTMLElement); + root.render( + + + + + , + ); +} else { + console.error('Target element not found.'); +} diff --git a/assets/react/v3/entries/pro/membership-settings/molecules/EmptyState.tsx b/assets/react/v3/entries/pro/membership-settings/molecules/EmptyState.tsx new file mode 100644 index 0000000000..58932d77a5 --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/molecules/EmptyState.tsx @@ -0,0 +1,62 @@ +import { css } from '@emotion/react'; +import { __ } from '@wordpress/i18n'; +import SVGIcon from '@Atoms/SVGIcon'; +import Button from '@Atoms/Button'; +import { borderRadius, colorTokens, lineHeight, spacing } from '@Config/styles'; +import { typography } from '@Config/typography'; +import emptyStateImage from '@Images/membership-empty-state.webp'; + +interface EmptyStateProps { + onActionClick: () => void; +} + +const EmptyState = ({ onActionClick }: EmptyStateProps) => { + return ( +
+ {__('No +
{__('No Membership Added Yet', 'tutor')}
+
{__('Set up memberships or package plans to sell on your site.', 'tutor')}
+ +
+ ); +}; + +export default EmptyState; + +const styles = { + wrapper: css` + display: flex; + align-items: center; + flex-direction: column; + gap: ${spacing[8]}; + + background-color: ${colorTokens.background.white}; + border: 1px solid ${colorTokens.stroke.divider}; + border-radius: ${borderRadius[6]}; + padding: ${spacing[32]} ${spacing[24]}; + + img { + max-width: 234px; + } + `, + title: css` + ${typography.heading6('medium')}; + line-height: ${lineHeight[28]}; + `, + content: css` + ${typography.body()}; + line-height: ${lineHeight[22]}; + color: ${colorTokens.text.title}; + margin-bottom: ${spacing[12]}; + max-width: 306px; + text-align: center; + `, +}; diff --git a/assets/react/v3/entries/pro/membership-settings/services/memberships.ts b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts new file mode 100644 index 0000000000..7b76cb5bef --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts @@ -0,0 +1,29 @@ +// import { wpAjaxInstance } from '@Utils/api'; +// import endpoints from '@Utils/endpoints'; +import { useQuery } from '@tanstack/react-query'; + +export interface Membership { + id: number; + name: string; + price: number; + duration: number; + duration_type: string; + description: string; + is_default: boolean; +} + +export interface MembershipSettings { + memberships: Membership[]; +} + +const getMembershipSettings = () => { + return Promise.resolve(null); + // return wpAjaxInstance.get(endpoints.GET_MEMBERSHIP_SETTINGS).then((response) => response.data); +}; + +export const useMembershipSettingsQuery = () => { + return useQuery({ + queryKey: ['MembershipSettings'], + queryFn: getMembershipSettings, + }); +}; diff --git a/assets/react/v3/public/images/membership-empty-state.webp b/assets/react/v3/public/images/membership-empty-state.webp new file mode 100644 index 0000000000000000000000000000000000000000..84cc835de005a3025d6e4cc1e5d770b7289981c3 GIT binary patch literal 5958 zcmV-M7rE$CNk&FK7XScPMM6+kP&iC67XSb+rNf;NpP-!3mt7@you^Kn=KFW}j!_k;D z{aOAYEieCh`OnLLUjFm)pO^o<{O9FAFaLS@&x;#c`ThIXugs=0; zmFhpDrpVNLnUR00CZ)~CEr`OeNx4puI@58TEVDKnxA;%!tn{wTY%c1`%%|wSO7TTmw^C=@XyQlc(J7 zY2#dr%a3DOlO;YSEa$l0lf=1Jndf6!b0a<_Fw@5EZuOz-mYefpSyLfCr8LvU?S70; z>COeQtbvbD4G$xKct9%;@hSbeGL|*WgRq0Z)v>JMjZX~(CSqBGItV)&n2BW#<{<1q zVAJg$MCY0c$Gg39_{Clb9g!O~3$*MWUa4c)2;#0nGHQ6;+ zcyYT2C_crD_m24%i$1Dv8-W;PkG17HXIlv zOdTE|f3iLToJ=mv9WG;8q7TBtz{ljoyx}&MC3buY3NE(q#+>0hmL>5ZEFL^ej?5SS zV_DJ;!ZPN=S2;WY#_g8WxyFV0ZFn8@B3&-i>T_@6pa$Qo350phHQUAn~XV_V{z;N8mq#8pV2U~E+Az<@`o)NkZDkJo(gIB zShvS;9N5}OL&v&f;-3=!eEn0zJ>$~O88+6P5<|FsR|58`c|^|SLdLpF;uqQvBvDU` zmN~=4x=GGjLuN%Ak#iR^vG>C!PcNNNqMj!LZj>SO`bgaIX^GK79+I_e;4GrJ5E~{{`!!$o_4%Dn_bIl!Rv_h*;u-=^!ZsZK& zRlb5*_k9A5EU!oW@W$=R0=1@4iT=SwAFy`<*1I#prQBq^IF=O~L6i5%;qXShT}uF& zOCgrr0eSSsFTmMZjC{4*0&3msxx$J}<3K z9^n+H&LY_DaKX3hEZFJioA{fT>P^8Ypd)DU?mzYJj+>40Y_hojqu47*3Oa64iFeZL zaKiU<%W@0|;$1t;i)V(%O^A1;t#^Oitd#4HzzfkwO_XJaM>Gd+I~)?s5T86s)_a8v zlMP8Sg^StVVO#GGx!Ea~W+Vp>3d)N32-ZMtha9_i^c$Ly?f+l zh;Z@PZ;(J)ntS1t)@TgOcDN*3;&yesIz;OV%6tldL|Z_%-dz$*5xDxhTne0t{vf$o0jC|B5SS}VLrE|KOzT|}ZqZsUgBj@7@VFin9ur0)f+;#_4C5{R)hBC_}K7O95BF zz{#@Ze5UqD*xY<$!6QZ3HwEz5X{0Krj#37o?rBEc%-_(^xU2xXQ_y9obT^xcb zMX-wD;!{7LbM9vx^!I0%QNZvfh?}8Y3dMv=8r4-`vqNecWQzZ7d2rAkfM&hpAYvo( zR+dKaOMC#C9a^(3n9IuVOL_p9^{zDqG#fiYdIXmpV1ABl*a&8FDHIce{r&zIz_Q-O zW{3TrMv^0@FF<7n90E4%)D$eA!u}(uuK;De%T0#O#%vzamte935Ro1gHknxMkiG%W z!uy{k!D+bP5L;qf5$$J%iETs7m8J38LK2Mt4Ks>T`4lnQZ-!}3P=J#iY9Y92O0p5j z(STFDb=`hil3)N7j41jDF$7n;d&K~F(@2Uxa>0k@Zmd8w2%c6yLua$^TzhOs(vhes z6*}3rbM4e66OX_Q@Ne|J*{<91<0kpwS5t;W{}_C?up}Ren<7KS82meA0urd@Qm7yV z1dT{I(B0WcioXa#gs`VqkObiiJ!DJ=9P9^htHPFo0HAaOehNn&-{Li}Z?K$Mb5>2&&;02>2hl9I&gZ2I^s5?CFR zm_$a;Ow$i=L; z5%Z>_ih!C0OBdtIMS#5Npdz4VDJ82lmAGPnFnpxIZz3HeSa8^ZA6e48o7ug{! ziNxzutVAf-uUIDvOY}Wi%BA2Vpx(5wq~Lb4rZ5q_Tz7}ry9i5S@T!UPXP6}V1Fs&1 zB|2R;r*Nr=eL+}KfIlTNpSTG0S5+53}-fxu!iN}1Mt=8mIb^=6YNx{cjo4FLy z31-I-q$OEjYgZveW3bUTa$srlRi{h_s|&b**uQQCTSv zRl}rVc)OPiB{GK>*QPDW;=Rl(lqehs+LG+u%YmYe;5#2RYD=`cWH-g1oPho0-I`(6 z+`pmsnG_odqPC=9W62Gb6d4imlJf^kh9~>8Wd&k6xo!X=y49UV!$WfdVM~oCg_;L*rg`D7< zm{f<<=jTK|;i*ieiA1Ky=9rLX5|tj=FCh&l-c$Vb6Mnw+kpogqer5McnoDGYY)(m& ziAs>{b6asX(HMR>fn9!yNqk03S(-;;noVpA!?OA#r0GO?2IXFQP?}FviWGMh(*VFI zmx80<%+d(JNFgb>T9sxLnIv0sv>4JDz)0a;(d2s{`S-BpIR(uRUAU(p#++hZSeAYA zl4-I|UJ6a}3)8X_ z=lz32VNRyX#+&1VMIlb5O6JW64c>Xvxc)YbaN+DZ_3I738XElgH2ZPv+3p-rBwG}@G5X7H4y4KqAa{CNvgl}wy% zs#1wlMj09@{=|h*hDA;0WuK^I>TDC0N}W7WY21I0MF*MTQ1N(Z?Yzz0Ya)9uK zCLM$+JQxpNnQ#60L2|yWPMo?MW3}sV*Wu2g83CI(aDgkAF zvM&Zf^6Df<5HyLBJ&D$3?lxcWd0%ER%AAN=a@0L`Tk zSMcdHC@guMaYoRmGs7bFRhX`JY1$w9b5A#GoN_u1{c> z=Wb{H%fSXoJFoMRr{?+JM>L8W6_GgC7xX!Ib@*bB zi?}mya!>(x-7+1G^LF8`H)2qRLWtpNXHc7w+WXb}U?;?pNi=D6c+|MRkORn-E?%8;;*_8gVk z@Rty`>tT0Sp?|ZAR)VXpKuWtiY&rH6QONEGVb!#|Q!mVx)I_0z>bhn7b9UF6FHu@$ zXbkY7Fo!?q8pZt{wCkkEiESs3=6RUAHlhBVUNcgV%t5_fJpp*HMU|9+LU!^wx`yq_8ic z@}#1U%F-_Q?gMwOqtm0g^3{7j=x%pBKbJyB0r}IzIX*IabGHiw(lBh~Qb>P|br25I zb0zFP4;^N5)q~1kqObSE^t=fbPG}D96#}SziM|XvvfbLwmt!o;Kux``gpk66!fNA; z=)$<&SsperU3Z9R+V6_u+zl8Nbt*ECPk9)kXjmS4V57-HWf7l3^@y$F2@)VygnQ{h ztebo@^XIm~d^cg-?ko~3YJZQ$IIRLhxkV&0+V~XI(8@;Q2w@K?{_+b<&TOz> zc%TyrX#kX^5s6RXX~TugLH*HiE`>@0@??EfxhWYyJA@An1P;C4LVI4Q& zD$p~FRH4Yu24Nx38=ki$f=QZM6N+kHi{EQ_b<^+f2b-bXYZP6D2)^s~)`D|-C@eek zc#WcJK7~yUT2kW0&R|hb=|yJP4zH!O^=10+np|`DQ!MQj1L;M zwawW6cT1lNyUsjbkV_$#AV=9AMi3QRDN%~Q+Xrk6YZ#3>=&{h`qDrx5D~4Nb-RP|l zS6P}2?RCrcy}z2*U57_AdaM*|msINgT_{qOr0bgcaWHE~1eoh)-!9%W9%gNyevucjk-~(P)~) zr<5Ja-Be>)NjulN$Fj0)WP0(b(Qvz~Y!t0~5>Ilw(CB$bA56%80T2lE@o>ihT~I%?{@E1&UUc- zaIOa~mStelePhpd-9zejUvv*aEn803AM{w(rfSnHE3iS>5j#9!|CEm?>w+=odT3)= zCtWnSvurl . 'assets/js/tutor-shared.min.js', array( 'wp-i18n', 'wp-element' ), TUTOR_VERSION, true ); - wp_enqueue_script( 'tutor-tax-settings.min', tutor()->url . 'assets/js/tutor-tax-settings.min.js', array( 'wp-i18n', 'wp-element', 'tutor-shared' ), TUTOR_VERSION, true ); - wp_enqueue_script( 'tutor-payment-settings.min', tutor()->url . 'assets/js/tutor-payment-settings.min.js', array( 'wp-i18n', 'wp-element', 'tutor-shared' ), TUTOR_VERSION, true ); + wp_enqueue_script( 'tutor-tax-settings.min', tutor()->url . 'assets/js/tutor-tax-settings.min.js', array( 'tutor-shared' ), TUTOR_VERSION, true ); + wp_enqueue_script( 'tutor-payment-settings.min', tutor()->url . 'assets/js/tutor-payment-settings.min.js', array( 'tutor-shared' ), TUTOR_VERSION, true ); + + // @since 3.2.0 membership settings options. + if ( tutor_utils()->is_addon_enabled( 'tutor-pro/addons/subscription/subscription.php' ) ) { + wp_enqueue_script( 'tutor-membership-settings.min', tutor()->url . 'assets/js/tutor-membership-settings.min.js', array( 'tutor-shared' ), TUTOR_VERSION, true ); + } } } } diff --git a/webpack.config.js b/webpack.config.js index bab9d5baca..9df043a527 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -84,9 +84,10 @@ module.exports = (env, options) => { 'tutor-gutenberg.min': './assets/react/gutenberg/index.js', 'tutor-course-builder.min': './assets/react/v3/entries/course-builder/index.tsx', 'tutor-order-details.min': './assets/react/v3/entries/order-details/index.tsx', + 'tutor-coupon.min': './assets/react/v3/entries/coupon-details/index.tsx', 'tutor-tax-settings.min': './assets/react/v3/entries/tax-settings/index.tsx', 'tutor-payment-settings.min': './assets/react/v3/entries/payment-settings/index.tsx', - 'tutor-coupon.min': './assets/react/v3/entries/coupon-details/index.tsx', + 'tutor-membership-settings.min': './assets/react/v3/entries/pro/membership-settings/index.tsx', }, clean: true, }, From 9a816486a63749c5c74faa226b89dc4c1d6182e9 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Thu, 26 Dec 2024 13:27:35 +0600 Subject: [PATCH 02/18] Create membership modal added with form fields --- .../components/MembershipSettings.tsx | 23 +- .../components/modals/SubscriptionModal.tsx | 493 ++++++++++++++++++ 2 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx index 6125748438..9d61f63b56 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx @@ -7,8 +7,13 @@ import { spacing } from '@Config/styles'; import { useFormWithGlobalError } from '@Hooks/useFormWithGlobalError'; import { type MembershipSettings, useMembershipSettingsQuery } from '../services/memberships'; import EmptyState from '../molecules/EmptyState'; +import { useModal } from '@/v3/shared/components/modals/Modal'; +import SubscriptionModal from './modals/SubscriptionModal'; +import SVGIcon from '@/v3/shared/atoms/SVGIcon'; +import { __ } from '@wordpress/i18n'; function MembershipSettings() { + const { showModal } = useModal(); const form = useFormWithGlobalError({ defaultValues: { memberships: [], @@ -43,7 +48,23 @@ function MembershipSettings() { return (
- console.log('here!')} />}> + { + showModal({ + component: SubscriptionModal, + props: { + title: __('Create Membership', 'tutor'), + icon: , + }, + depthIndex: 9999, + }); + }} + /> + } + > Membership list diff --git a/assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx b/assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx new file mode 100644 index 0000000000..2495b54724 --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx @@ -0,0 +1,493 @@ +import { css } from '@emotion/react'; +import { __, sprintf } from '@wordpress/i18n'; +import { Controller, FormProvider } from 'react-hook-form'; + +import Button from '@Atoms/Button'; +import SVGIcon from '@Atoms/SVGIcon'; + +import type { ModalProps } from '@Components/modals/Modal'; +import ModalWrapper from '@Components/modals/ModalWrapper'; + +import { borderRadius, Breakpoint, colorTokens, spacing } from '@Config/styles'; +import { typography } from '@Config/typography'; +import { useFormWithGlobalError } from '@Hooks/useFormWithGlobalError'; +import FormInput from '@/v3/shared/components/fields/FormInput'; +import FormInputWithContent from '@/v3/shared/components/fields/FormInputWithContent'; +import { styleUtils } from '@/v3/shared/utils/style-utils'; +import FormSelectInput from '@/v3/shared/components/fields/FormSelectInput'; +import { requiredRule } from '@Utils/validation'; +import { tutorConfig } from '@/v3/shared/config/config'; +import FormInputWithPresets from '@/v3/shared/components/fields/FormInputWithPresets'; +import FormCheckbox from '@/v3/shared/components/fields/FormCheckbox'; +import Show from '@/v3/shared/controls/Show'; +import FormTimeInput from '@/v3/shared/components/fields/FormTimeInput'; +import FormDateInput from '@/v3/shared/components/fields/FormDateInput'; +import FormSwitch from '@/v3/shared/components/fields/FormSwitch'; +const { tutor_currency } = tutorConfig; + +interface SubscriptionModalProps extends ModalProps { + closeModal: (props?: { action: 'CONFIRM' | 'CLOSE' }) => void; +} + +export default function SubscriptionModal({ title, subtitle, icon, closeModal }: SubscriptionModalProps) { + const form = useFormWithGlobalError({ + defaultValues: { + plan_name: '', + short_description: '', + description: '', + categories: [], + payment_type: '', + plan_type: '', + recurring_value: '', + recurring_interval: '', + recurring_limit: '', + regular_price: '', + offer_sale_price: false, + sale_price: '', + schedule_sale_price: false, + sale_price_from: '', + sale_price_from_date: '', + sale_price_from_time: '', + sale_price_to: '', + sale_price_to_date: '', + sale_price_to_time: '', + charge_enrollment_fee: false, + enrollment_fee: '', + provide_certificate: false, + is_featured: false, + }, + mode: 'onChange', + }); + + const isFormDirty = form.formState.isDirty; + + const chargeEnrolmentFee = form.watch('charge_enrollment_fee'); + const hasSale = form.watch('offer_sale_price'); + const regularPrice = form.watch(`regular_price`); + const hasScheduledSale = !!form.watch(`schedule_sale_price`); + + const lifetimePresets = [3, 6, 9, 12]; + const lifetimeOptions = [ + ...lifetimePresets.map((preset) => ({ + label: sprintf(__('%s times', 'tutor'), preset.toString()), + value: String(preset), + })), + { + label: __('Until cancelled', 'tutor'), + value: 'Until cancelled', + }, + ]; + + return ( + + closeModal({ action: 'CLOSE' })} + icon={isFormDirty ? : icon} + title={isFormDirty ? __('Unsaved Changes', 'tutor') : title} + subtitle={isFormDirty ? title?.toString() : subtitle} + actions={ + <> + + + + } + > +
+
+ ( + + )} + /> + + ( + + )} + /> + +
+ { + if (Number(value) <= 0) { + return __('Price must be greater than 0', 'tutor'); + } + }, + }} + render={(controllerProps) => ( + + )} + /> + + { + if (Number(value) < 1) { + return __('This value must be equal to or greater than 1', 'tutor'); + } + }, + }} + render={(controllerProps) => ( + + )} + /> + + ( +  
} + options={[ + { label: __('Day(s)', 'tutor'), value: 'day' }, + { label: __('Week(s)', 'tutor'), value: 'week' }, + { label: __('Month(s)', 'tutor'), value: 'month' }, + { label: __('Year(s)', 'tutor'), value: 'year' }, + ]} + removeOptionsMinWidth + /> + )} + /> + + { + if (value === 'Until cancelled') { + return true; + } + + if (Number(value) <= 0) { + return __('Renew plan must be greater than 0', 'tutor'); + } + return true; + }, + }} + render={(controllerProps) => ( + + )} + /> +
+ + ( + + )} + /> + + ( + + )} + /> + + + { + if (Number(value) <= 0) { + return __('Enrollment fee must be greater than 0', 'tutor'); + } + return true; + }, + }} + render={(controllerProps) => ( + + )} + /> + + + ( + + )} + /> + + ( + + )} + /> + +
+
+ } + /> +
+ +
+ { + if (value && regularPrice && Number(value) >= Number(regularPrice)) { + return __('Sale price should be less than regular price', 'tutor'); + } + + if (value && regularPrice && Number(value) <= 0) { + return __('Sale price should be greater than 0', 'tutor'); + } + + return undefined; + }, + }} + render={(props) => ( + + )} + /> + + } + /> + + +
+ +
+ ( + + )} + /> + + ( + + )} + /> +
+
+
+ +
+ { + const startDate = form.watch(`sale_price_from_date`); + const endDate = value; + if (startDate && endDate) { + return new Date(startDate) > new Date(endDate) + ? __('Sales End date should be greater than start date', 'tutor') + : undefined; + } + return undefined; + }, + }, + deps: ['sale_price_from_date'], + }} + render={(controllerProps) => ( + + )} + /> + + { + const startDate = form.watch(`sale_price_from_date`); + const startTime = form.watch(`sale_price_from_time`); + const endDate = form.watch(`sale_price_to_date`); + const endTime = value; + if (startDate && endDate && startTime && endTime) { + return new Date(`${startDate} ${startTime}`) > new Date(`${endDate} ${endTime}`) + ? __('Sales End time should be greater than start time', 'tutor') + : undefined; + } + return undefined; + }, + }, + deps: ['sale_price_from_date', 'sale_price_from_time', 'sale_price_to_date'], + }} + render={(controllerProps) => ( + + )} + /> +
+
+
+
+
+
+
+
+ + + ); +} + +const styles = { + wrapper: css` + padding: ${spacing[40]} ${spacing[16]}; + + ${Breakpoint.mobile} { + padding: ${spacing[24]} ${spacing[16]}; + } + `, + container: css` + width: 100%; + max-width: 640px; + margin: 0 auto; + border: 1px solid ${colorTokens.stroke.default}; + border-radius: ${borderRadius.card}; + padding: ${spacing[16]}; + + display: flex; + flex-direction: column; + gap: ${spacing[12]}; + `, + salePriceWrapper: css` + background-color: ${colorTokens.background.white}; + display: flex; + flex-direction: column; + gap: ${spacing[20]}; + + padding: ${spacing[12]}; + border: 1px solid ${colorTokens.stroke.default}; + border-radius: ${borderRadius.card}; + `, + salePriceInputs: css` + display: flex; + flex-direction: column; + gap: ${spacing[8]}; + `, + inputGroup: css` + display: grid; + grid-template-columns: 1fr 0.7fr 1fr 1fr; + align-items: start; + gap: ${spacing[8]}; + `, + datetimeWrapper: css` + label { + ${typography.caption()}; + color: ${colorTokens.text.title}; + } + `, +}; From ff1f07659d4b2117cf3daddc049dffc1f2888f38 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Fri, 27 Dec 2024 15:02:53 +0600 Subject: [PATCH 03/18] display-name removed from eslint --- eslint.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 0e6f8b0a25..b986a76569 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,6 +25,7 @@ export default [ 'react/react-in-jsx-scope': 'off', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', + 'react/display-name': 'off', '@typescript-eslint/consistent-type-imports': [ 'error', { From 7168ad5e822d0ce3b0dc68bcb64ba2ff1cbe8862 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Fri, 27 Dec 2024 15:03:05 +0600 Subject: [PATCH 04/18] FormIconsAndFeatures field added --- .../fields/FormIconsAndFeatures.tsx | 314 ++++++++++++++++++ .../components/modals/SubscriptionModal.tsx | 12 +- .../membership-settings/config/constants.ts | 29 ++ 3 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 assets/react/v3/entries/pro/membership-settings/components/fields/FormIconsAndFeatures.tsx create mode 100644 assets/react/v3/entries/pro/membership-settings/config/constants.ts diff --git a/assets/react/v3/entries/pro/membership-settings/components/fields/FormIconsAndFeatures.tsx b/assets/react/v3/entries/pro/membership-settings/components/fields/FormIconsAndFeatures.tsx new file mode 100644 index 0000000000..c322c65dba --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/components/fields/FormIconsAndFeatures.tsx @@ -0,0 +1,314 @@ +import Button from '@/v3/shared/atoms/Button'; +import SVGIcon from '@/v3/shared/atoms/SVGIcon'; +import FormFieldWrapper from '@/v3/shared/components/fields/FormFieldWrapper'; +import { borderRadius, colorTokens, fontSize, lineHeight, shadow, spacing, zIndex } from '@/v3/shared/config/styles'; +import { typography } from '@/v3/shared/config/typography'; +import For from '@/v3/shared/controls/For'; +import Show from '@/v3/shared/controls/Show'; +import { type FormControllerProps } from '@/v3/shared/utils/form'; +import { css } from '@emotion/react'; +import { useState } from 'react'; +import { featureIcons } from '../../config/constants'; +import { Portal, usePortalPopover } from '@/v3/shared/hooks/usePortalPopover'; +import { isRTL } from '@/v3/shared/config/constants'; +import { __ } from '@wordpress/i18n'; + +interface Feature { + icon: string; + content: string; +} + +interface FeatureItemProps { + data: Feature; + handleContentChange: (value: string) => void; + handleIconChange: (value: keyof typeof featureIcons) => void; + handleDeleteClick: () => void; +} + +const FeatureItem = ({ data, handleContentChange, handleIconChange, handleDeleteClick }: FeatureItemProps) => { + const [isOpen, setIsOpen] = useState(false); + const { triggerRef, position, popoverRef } = usePortalPopover({ + isOpen, + }); + + return ( + <> +
+ +
+ { + setIsOpen(false); + }} + onEscape={() => { + setIsOpen(false); + }} + > +
+
+ + +
+
+ + {(icon: keyof typeof featureIcons) => { + return ( +
+
+
+ + ); +}; + +interface FormIconsAndFeaturesProps extends FormControllerProps { + label: string; +} + +function FormIconsAndFeatures({ label, field, fieldState }: FormIconsAndFeaturesProps) { + const fieldValue = field.value || []; + + return ( + + {() => { + return ( +
+
+ + +
+ 0}> +
+ + {(feature, index) => { + return ( + { + field.onChange( + [...fieldValue].map((item, idx) => { + return idx === index ? { ...item, icon: featureIcons[value] } : item; + }), + ); + }} + handleContentChange={(value) => { + field.onChange( + [...fieldValue].map((item, idx) => { + return idx === index ? { ...item, content: value } : item; + }), + ); + }} + handleDeleteClick={() => { + field.onChange(fieldValue.filter((_, idx) => idx !== index)); + }} + /> + ); + }} + +
+
+
+ ); + }} +
+ ); +} + +export default FormIconsAndFeatures; + +const styles = { + wrapper: css` + background-color: ${colorTokens.background.white}; + border: 1px solid ${colorTokens.stroke.divider}; + border-radius: ${borderRadius[6]}; + padding: ${spacing[12]} ${spacing[16]}; + `, + header: css` + display: flex; + align-items: center; + justify-content: space-between; + + label { + ${typography.caption()}; + color: ${colorTokens.text.title}; + } + + button { + color: ${colorTokens.icon.default}; + border: 1px solid ${colorTokens.stroke.default}; + border-radius: ${borderRadius[4]}; + padding: 3px; + } + `, + features: css` + display: flex; + flex-direction: column; + gap: ${spacing[8]}; + padding: ${spacing[12]} 0 ${spacing[8]}; + `, + featureItem: css` + position: relative; + display: flex; + + &:hover { + button[data-delete-button] { + opacity: 1; + } + } + + input { + width: 100%; + font-size: ${fontSize[16]}; + line-height: ${lineHeight[24]}; + border: 1px solid ${colorTokens.stroke.default}; + border-top-right-radius: ${borderRadius[6]}; + border-bottom-right-radius: ${borderRadius[6]}; + border-left: none; + padding: ${spacing[4]} ${spacing[36]} ${spacing[4]} ${spacing[8]}; + + &:focus { + border-radius: ${borderRadius[6]}; + outline: 2px solid ${colorTokens.stroke.brand}; + outline-offset: 2px; + } + } + `, + iconSelector: css` + height: 40px; + display: flex; + align-items: center; + background-color: transparent; + color: ${colorTokens.icon.hover}; + border: 1px solid ${colorTokens.stroke.default}; + border-top-left-radius: ${borderRadius[6]}; + border-bottom-left-radius: ${borderRadius[6]}; + cursor: pointer; + transition: background-color 0.25s; + + :hover { + background-color: ${colorTokens.background.hover}; + } + + :focus-visible { + border-radius: ${borderRadius[6]}; + outline: 2px solid ${colorTokens.stroke.brand}; + outline-offset: 2px; + z-index: 1; + } + `, + deleteButton: css` + display: flex; + position: absolute; + right: ${spacing[12]}; + top: ${spacing[8]}; + padding: 0; + color: ${colorTokens.icon.default}; + background: transparent; + border: none; + cursor: pointer; + opacity: 0; + transition: opacity 0.25s; + + :focus-visible { + border-radius: ${borderRadius[2]}; + outline: 2px solid ${colorTokens.stroke.brand}; + outline-offset: 2px; + opacity: 1; + } + `, + popoverWrapper: css` + position: absolute; + width: 100%; + z-index: ${zIndex.dropdown}; + background-color: ${colorTokens.background.white}; + box-shadow: ${shadow.popover}; + border-radius: ${borderRadius[6]}; + max-height: 300px; + overflow-y: auto; + `, + popoverHeader: css` + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid ${colorTokens.stroke.divider}; + padding: ${spacing[8]}; + + label { + ${typography.caption('medium')}; + color: ${colorTokens.text.title}; + } + + button { + padding: 0px; + } + `, + popoverContent: css` + display: flex; + flex-wrap: wrap; + gap: ${spacing[8]}; + padding: ${spacing[12]}; + `, + popoverContentButton: css` + display: flex; + background-color: ${colorTokens.background.default}; + color: ${colorTokens.icon.hover}; + border: none; + border-radius: ${borderRadius[4]}; + padding: ${spacing[8]}; + cursor: pointer; + transition: + background-color 0.25s, + box-shadow 0.25s; + + :hover { + background-color: ${colorTokens.background.hover}; + box-shadow: inset 0px 0px 0px 1px ${colorTokens.action.primary.hover}; + } + + :focus-visible { + border-radius: ${borderRadius[6]}; + outline: 2px solid ${colorTokens.stroke.brand}; + outline-offset: 2px; + z-index: 1; + } + `, +}; diff --git a/assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx b/assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx index 2495b54724..03eb877697 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx @@ -23,6 +23,7 @@ import Show from '@/v3/shared/controls/Show'; import FormTimeInput from '@/v3/shared/components/fields/FormTimeInput'; import FormDateInput from '@/v3/shared/components/fields/FormDateInput'; import FormSwitch from '@/v3/shared/components/fields/FormSwitch'; +import FormIconsAndFeatures from '../fields/FormIconsAndFeatures'; const { tutor_currency } = tutorConfig; interface SubscriptionModalProps extends ModalProps { @@ -34,7 +35,7 @@ export default function SubscriptionModal({ title, subtitle, icon, closeModal }: defaultValues: { plan_name: '', short_description: '', - description: '', + features: [], categories: [], payment_type: '', plan_type: '', @@ -56,7 +57,6 @@ export default function SubscriptionModal({ title, subtitle, icon, closeModal }: provide_certificate: false, is_featured: false, }, - mode: 'onChange', }); const isFormDirty = form.formState.isDirty; @@ -235,6 +235,14 @@ export default function SubscriptionModal({ title, subtitle, icon, closeModal }: )} /> + ( + + )} + /> + ', + crossCircle: + '', + tick: '', + cross: + '', + plusSquare: + '', + minusSquare: + '', + plusCircle: + '', + minusCircle: + '', + tickCircleFill: + '', + crossCircleFill: + '', + plusCircleFill: + '', + minusCircleFill: + '', + plusSquareFill: + '', + minusSquareFill: + '', +}; From dc871cf446ff401116bfdd56825ab15abcaf22ce Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Fri, 27 Dec 2024 22:43:40 +0600 Subject: [PATCH 05/18] Field array added for features --- .../components/IconsAndFeatures.tsx | 112 +++++++ .../components/fields/FormFeatureItem.tsx | 274 +++++++++++++++ .../fields/FormIconsAndFeatures.tsx | 314 ------------------ .../components/modals/SubscriptionModal.tsx | 20 +- 4 files changed, 397 insertions(+), 323 deletions(-) create mode 100644 assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx create mode 100644 assets/react/v3/entries/pro/membership-settings/components/fields/FormFeatureItem.tsx delete mode 100644 assets/react/v3/entries/pro/membership-settings/components/fields/FormIconsAndFeatures.tsx diff --git a/assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx b/assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx new file mode 100644 index 0000000000..437dae88ce --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx @@ -0,0 +1,112 @@ +import Button from '@/v3/shared/atoms/Button'; +import SVGIcon from '@/v3/shared/atoms/SVGIcon'; +import { borderRadius, colorTokens, spacing } from '@/v3/shared/config/styles'; +import { typography } from '@/v3/shared/config/typography'; +import For from '@/v3/shared/controls/For'; +import Show from '@/v3/shared/controls/Show'; +import { css } from '@emotion/react'; +import { __ } from '@wordpress/i18n'; +import { featureIcons } from '../config/constants'; +import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; +import FormFeatureItem from './fields/FormFeatureItem'; +import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { nanoid } from '@/v3/shared/utils/util'; +import { restrictToParentElement } from '@dnd-kit/modifiers'; + +export default function IconsAndFeatures() { + const form = useFormContext(); + const { fields, append, remove, move } = useFieldArray({ + control: form.control, + name: 'features', + }); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + return ( +
+
+ + +
+ 0}> +
+ { + const { active, over } = event; + if (!over) { + return; + } + + if (active.id !== over.id) { + const activeIndex = fields.findIndex((item) => item.id === active.id); + const overIndex = fields.findIndex((item) => item.id === over.id); + + move(activeIndex, overIndex); + } + }} + > + + + {(item, index) => ( + !!value?.content || __('Content is required', 'tutor'), + }} + render={(controllerProps) => ( + remove(index)} /> + )} + /> + )} + + + +
+
+
+ ); +} + +const styles = { + wrapper: css` + background-color: ${colorTokens.background.white}; + border: 1px solid ${colorTokens.stroke.divider}; + border-radius: ${borderRadius[6]}; + padding: ${spacing[12]} ${spacing[16]}; + `, + header: css` + display: flex; + align-items: center; + justify-content: space-between; + + label { + ${typography.caption()}; + color: ${colorTokens.text.title}; + } + + button { + color: ${colorTokens.icon.default}; + border: 1px solid ${colorTokens.stroke.default}; + border-radius: ${borderRadius[4]}; + padding: 3px; + } + `, + features: css` + display: flex; + flex-direction: column; + gap: ${spacing[8]}; + padding: ${spacing[12]} 0 ${spacing[8]}; + `, +}; diff --git a/assets/react/v3/entries/pro/membership-settings/components/fields/FormFeatureItem.tsx b/assets/react/v3/entries/pro/membership-settings/components/fields/FormFeatureItem.tsx new file mode 100644 index 0000000000..edc1687247 --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/components/fields/FormFeatureItem.tsx @@ -0,0 +1,274 @@ +import Button from '@/v3/shared/atoms/Button'; +import SVGIcon from '@/v3/shared/atoms/SVGIcon'; +import FormFieldWrapper from '@/v3/shared/components/fields/FormFieldWrapper'; +import { animateLayoutChanges } from '@/v3/shared/utils/dndkit'; +import { borderRadius, Breakpoint, colorTokens, shadow, spacing, zIndex } from '@/v3/shared/config/styles'; +import { typography } from '@/v3/shared/config/typography'; +import For from '@/v3/shared/controls/For'; +import { type FormControllerProps } from '@/v3/shared/utils/form'; +import { css } from '@emotion/react'; +import { CSS } from '@dnd-kit/utilities'; +import { useState } from 'react'; +import { featureIcons } from '../../config/constants'; +import { Portal, usePortalPopover } from '@/v3/shared/hooks/usePortalPopover'; +import { isRTL } from '@/v3/shared/config/constants'; +import { __ } from '@wordpress/i18n'; +import { useSortable } from '@dnd-kit/sortable'; + +interface Feature { + id: string; + icon: string; + content: string; +} + +interface FormFeatureItemProps extends FormControllerProps { + id: string; + handleDeleteClick: () => void; +} + +export default function FormFeatureItem({ id, field, fieldState, handleDeleteClick }: FormFeatureItemProps) { + const [isOpen, setIsOpen] = useState(false); + const { triggerRef, position, popoverRef } = usePortalPopover({ + isOpen, + }); + + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: id, + animateLayoutChanges, + }); + + const style = { + transform: CSS.Transform.toString(transform ? { ...transform, scaleX: 1, scaleY: 1 } : null), + transition, + zIndex: isDragging ? 1 : 0, + }; + + function handleIconChange(icon: keyof typeof featureIcons) { + field.onChange({ ...field.value, icon: featureIcons[icon] }); + } + + function handleContentChange(content: string) { + field.onChange({ ...field.value, content }); + } + + return ( +
+ + {(inputProps) => { + return ( + <> +
+ + +
+ + { + setIsOpen(false); + }} + onEscape={() => { + setIsOpen(false); + }} + > +
+
+ + +
+
+ + {(icon: keyof typeof featureIcons) => { + return ( +
+
+
+ + ); + }} +
+
+ ); +} + +const styles = { + featureItem: css` + position: relative; + display: flex; + + &:hover { + button[data-delete-button] { + opacity: 1; + } + } + `, + input: css` + &.tutor-input-field { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + padding: ${spacing[4]} ${spacing[36]} ${spacing[4]} ${spacing[8]}; + + &:focus { + border-radius: ${borderRadius[6]}; + } + } + `, + iconSelector: css` + height: 40px; + display: flex; + align-items: center; + background-color: ${colorTokens.background.white}; + color: ${colorTokens.icon.hover}; + border: 1px solid ${colorTokens.stroke.default}; + border-right: none; + border-top-left-radius: ${borderRadius[6]}; + border-bottom-left-radius: ${borderRadius[6]}; + cursor: pointer; + transition: background-color 0.25s; + + :hover { + background-color: ${colorTokens.background.hover}; + } + + :focus-visible { + border-radius: ${borderRadius[4]}; + outline: 2px solid ${colorTokens.stroke.brand}; + outline-offset: 2px; + z-index: 1; + } + `, + dragButton: css` + display: flex; + align-items: center; + padding: 0; + color: ${colorTokens.icon.default}; + background: transparent; + border: none; + cursor: grab; + + :focus-visible { + border-radius: ${borderRadius[4]}; + outline: 2px solid ${colorTokens.stroke.brand}; + } + `, + deleteButton: css` + display: flex; + position: absolute; + right: ${spacing[12]}; + top: ${spacing[8]}; + padding: 0; + color: ${colorTokens.icon.default}; + background: transparent; + border: none; + cursor: pointer; + opacity: 0; + transition: opacity 0.25s; + + :focus-visible { + border-radius: ${borderRadius[2]}; + outline: 2px solid ${colorTokens.stroke.brand}; + outline-offset: 2px; + opacity: 1; + } + + ${Breakpoint.mobile} { + opacity: 1; + } + `, + popoverWrapper: css` + position: absolute; + width: 100%; + z-index: ${zIndex.dropdown}; + background-color: ${colorTokens.background.white}; + box-shadow: ${shadow.popover}; + border-radius: ${borderRadius[6]}; + max-height: 300px; + overflow-y: auto; + `, + popoverHeader: css` + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid ${colorTokens.stroke.divider}; + padding: ${spacing[8]}; + + label { + ${typography.caption('medium')}; + color: ${colorTokens.text.title}; + } + + button { + padding: 0px; + } + `, + popoverContent: css` + display: flex; + flex-wrap: wrap; + gap: ${spacing[8]}; + padding: ${spacing[12]}; + `, + popoverContentButton: css` + display: flex; + background-color: ${colorTokens.background.default}; + color: ${colorTokens.icon.hover}; + border: none; + border-radius: ${borderRadius[4]}; + padding: ${spacing[8]}; + cursor: pointer; + transition: + background-color 0.25s, + box-shadow 0.25s; + + :hover { + background-color: ${colorTokens.background.hover}; + box-shadow: inset 0px 0px 0px 1px ${colorTokens.action.primary.hover}; + } + + :focus-visible { + border-radius: ${borderRadius[6]}; + outline: 2px solid ${colorTokens.stroke.brand}; + outline-offset: 2px; + z-index: 1; + } + `, +}; diff --git a/assets/react/v3/entries/pro/membership-settings/components/fields/FormIconsAndFeatures.tsx b/assets/react/v3/entries/pro/membership-settings/components/fields/FormIconsAndFeatures.tsx deleted file mode 100644 index c322c65dba..0000000000 --- a/assets/react/v3/entries/pro/membership-settings/components/fields/FormIconsAndFeatures.tsx +++ /dev/null @@ -1,314 +0,0 @@ -import Button from '@/v3/shared/atoms/Button'; -import SVGIcon from '@/v3/shared/atoms/SVGIcon'; -import FormFieldWrapper from '@/v3/shared/components/fields/FormFieldWrapper'; -import { borderRadius, colorTokens, fontSize, lineHeight, shadow, spacing, zIndex } from '@/v3/shared/config/styles'; -import { typography } from '@/v3/shared/config/typography'; -import For from '@/v3/shared/controls/For'; -import Show from '@/v3/shared/controls/Show'; -import { type FormControllerProps } from '@/v3/shared/utils/form'; -import { css } from '@emotion/react'; -import { useState } from 'react'; -import { featureIcons } from '../../config/constants'; -import { Portal, usePortalPopover } from '@/v3/shared/hooks/usePortalPopover'; -import { isRTL } from '@/v3/shared/config/constants'; -import { __ } from '@wordpress/i18n'; - -interface Feature { - icon: string; - content: string; -} - -interface FeatureItemProps { - data: Feature; - handleContentChange: (value: string) => void; - handleIconChange: (value: keyof typeof featureIcons) => void; - handleDeleteClick: () => void; -} - -const FeatureItem = ({ data, handleContentChange, handleIconChange, handleDeleteClick }: FeatureItemProps) => { - const [isOpen, setIsOpen] = useState(false); - const { triggerRef, position, popoverRef } = usePortalPopover({ - isOpen, - }); - - return ( - <> -
- -
- { - setIsOpen(false); - }} - onEscape={() => { - setIsOpen(false); - }} - > -
-
- - -
-
- - {(icon: keyof typeof featureIcons) => { - return ( -
-
-
- - ); -}; - -interface FormIconsAndFeaturesProps extends FormControllerProps { - label: string; -} - -function FormIconsAndFeatures({ label, field, fieldState }: FormIconsAndFeaturesProps) { - const fieldValue = field.value || []; - - return ( - - {() => { - return ( -
-
- - -
- 0}> -
- - {(feature, index) => { - return ( - { - field.onChange( - [...fieldValue].map((item, idx) => { - return idx === index ? { ...item, icon: featureIcons[value] } : item; - }), - ); - }} - handleContentChange={(value) => { - field.onChange( - [...fieldValue].map((item, idx) => { - return idx === index ? { ...item, content: value } : item; - }), - ); - }} - handleDeleteClick={() => { - field.onChange(fieldValue.filter((_, idx) => idx !== index)); - }} - /> - ); - }} - -
-
-
- ); - }} -
- ); -} - -export default FormIconsAndFeatures; - -const styles = { - wrapper: css` - background-color: ${colorTokens.background.white}; - border: 1px solid ${colorTokens.stroke.divider}; - border-radius: ${borderRadius[6]}; - padding: ${spacing[12]} ${spacing[16]}; - `, - header: css` - display: flex; - align-items: center; - justify-content: space-between; - - label { - ${typography.caption()}; - color: ${colorTokens.text.title}; - } - - button { - color: ${colorTokens.icon.default}; - border: 1px solid ${colorTokens.stroke.default}; - border-radius: ${borderRadius[4]}; - padding: 3px; - } - `, - features: css` - display: flex; - flex-direction: column; - gap: ${spacing[8]}; - padding: ${spacing[12]} 0 ${spacing[8]}; - `, - featureItem: css` - position: relative; - display: flex; - - &:hover { - button[data-delete-button] { - opacity: 1; - } - } - - input { - width: 100%; - font-size: ${fontSize[16]}; - line-height: ${lineHeight[24]}; - border: 1px solid ${colorTokens.stroke.default}; - border-top-right-radius: ${borderRadius[6]}; - border-bottom-right-radius: ${borderRadius[6]}; - border-left: none; - padding: ${spacing[4]} ${spacing[36]} ${spacing[4]} ${spacing[8]}; - - &:focus { - border-radius: ${borderRadius[6]}; - outline: 2px solid ${colorTokens.stroke.brand}; - outline-offset: 2px; - } - } - `, - iconSelector: css` - height: 40px; - display: flex; - align-items: center; - background-color: transparent; - color: ${colorTokens.icon.hover}; - border: 1px solid ${colorTokens.stroke.default}; - border-top-left-radius: ${borderRadius[6]}; - border-bottom-left-radius: ${borderRadius[6]}; - cursor: pointer; - transition: background-color 0.25s; - - :hover { - background-color: ${colorTokens.background.hover}; - } - - :focus-visible { - border-radius: ${borderRadius[6]}; - outline: 2px solid ${colorTokens.stroke.brand}; - outline-offset: 2px; - z-index: 1; - } - `, - deleteButton: css` - display: flex; - position: absolute; - right: ${spacing[12]}; - top: ${spacing[8]}; - padding: 0; - color: ${colorTokens.icon.default}; - background: transparent; - border: none; - cursor: pointer; - opacity: 0; - transition: opacity 0.25s; - - :focus-visible { - border-radius: ${borderRadius[2]}; - outline: 2px solid ${colorTokens.stroke.brand}; - outline-offset: 2px; - opacity: 1; - } - `, - popoverWrapper: css` - position: absolute; - width: 100%; - z-index: ${zIndex.dropdown}; - background-color: ${colorTokens.background.white}; - box-shadow: ${shadow.popover}; - border-radius: ${borderRadius[6]}; - max-height: 300px; - overflow-y: auto; - `, - popoverHeader: css` - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid ${colorTokens.stroke.divider}; - padding: ${spacing[8]}; - - label { - ${typography.caption('medium')}; - color: ${colorTokens.text.title}; - } - - button { - padding: 0px; - } - `, - popoverContent: css` - display: flex; - flex-wrap: wrap; - gap: ${spacing[8]}; - padding: ${spacing[12]}; - `, - popoverContentButton: css` - display: flex; - background-color: ${colorTokens.background.default}; - color: ${colorTokens.icon.hover}; - border: none; - border-radius: ${borderRadius[4]}; - padding: ${spacing[8]}; - cursor: pointer; - transition: - background-color 0.25s, - box-shadow 0.25s; - - :hover { - background-color: ${colorTokens.background.hover}; - box-shadow: inset 0px 0px 0px 1px ${colorTokens.action.primary.hover}; - } - - :focus-visible { - border-radius: ${borderRadius[6]}; - outline: 2px solid ${colorTokens.stroke.brand}; - outline-offset: 2px; - z-index: 1; - } - `, -}; diff --git a/assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx b/assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx index 03eb877697..1acb406c91 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/modals/SubscriptionModal.tsx @@ -23,7 +23,7 @@ import Show from '@/v3/shared/controls/Show'; import FormTimeInput from '@/v3/shared/components/fields/FormTimeInput'; import FormDateInput from '@/v3/shared/components/fields/FormDateInput'; import FormSwitch from '@/v3/shared/components/fields/FormSwitch'; -import FormIconsAndFeatures from '../fields/FormIconsAndFeatures'; +import IconsAndFeatures from '../IconsAndFeatures'; const { tutor_currency } = tutorConfig; interface SubscriptionModalProps extends ModalProps { @@ -97,7 +97,15 @@ export default function SubscriptionModal({ title, subtitle, icon, closeModal }: > {__('Cancel', 'tutor')} - @@ -235,13 +243,7 @@ export default function SubscriptionModal({ title, subtitle, icon, closeModal }: )} /> - ( - - )} - /> + Date: Mon, 30 Dec 2024 16:27:34 +0600 Subject: [PATCH 06/18] Membership save api integration added --- .../components/IconsAndFeatures.tsx | 7 +- .../components/MembershipItem.tsx | 35 +++ .../components/MembershipList.tsx | 36 +++ .../components/MembershipSettings.tsx | 44 ++-- .../components/fields/FormFeatureItem.tsx | 6 +- .../components/modals/SubscriptionModal.tsx | 116 +++++++--- .../membership-settings/config/constants.ts | 24 +- .../services/memberships.ts | 210 ++++++++++++++++-- assets/react/v3/shared/utils/endpoints.ts | 5 + assets/react/v3/shared/utils/types.ts | 8 +- 10 files changed, 397 insertions(+), 94 deletions(-) create mode 100644 assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx create mode 100644 assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx diff --git a/assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx b/assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx index 437dae88ce..660f0f477a 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx @@ -31,8 +31,11 @@ export default function IconsAndFeatures() { return (
- -
diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx new file mode 100644 index 0000000000..071bebf164 --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx @@ -0,0 +1,35 @@ +import Button from '@/v3/shared/atoms/Button'; +import { type MembershipPlan } from '../services/memberships'; +import SubscriptionModal from './modals/SubscriptionModal'; +import { __ } from '@wordpress/i18n'; +import SVGIcon from '@/v3/shared/atoms/SVGIcon'; +import { useModal } from '@/v3/shared/components/modals/Modal'; + +interface MembershipItemProps { + data: MembershipPlan; +} + +export default function MembershipItem({ data }: MembershipItemProps) { + const { showModal } = useModal(); + return ( +
+
{data.plan_name}
+
${data.regular_price} per month
+ +
+ ); +} diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx new file mode 100644 index 0000000000..5350539582 --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx @@ -0,0 +1,36 @@ +import Button from '@/v3/shared/atoms/Button'; +import SVGIcon from '@/v3/shared/atoms/SVGIcon'; +import For from '@/v3/shared/controls/For'; +import { __ } from '@wordpress/i18n'; +import { useFormContext } from 'react-hook-form'; +import MembershipItem from './MembershipItem'; +import { type MembershipSettings } from '../services/memberships'; + +interface MembershipListProps { + onNewMembershipClick: () => void; +} + +export default function MembershipList({ onNewMembershipClick }: MembershipListProps) { + const form = useFormContext(); + const membershipPlans = form.watch('plans'); + + console.log(form.getValues()); + + return ( +
+
+ {(item) => } +
+ + +
+ ); +} diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx index 9d61f63b56..32c61df7e3 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx @@ -11,12 +11,13 @@ import { useModal } from '@/v3/shared/components/modals/Modal'; import SubscriptionModal from './modals/SubscriptionModal'; import SVGIcon from '@/v3/shared/atoms/SVGIcon'; import { __ } from '@wordpress/i18n'; +import MembershipList from './MembershipList'; function MembershipSettings() { const { showModal } = useModal(); const form = useFormWithGlobalError({ defaultValues: { - memberships: [], + plans: [], }, }); @@ -24,9 +25,7 @@ function MembershipSettings() { const membershipSettingsQuery = useMembershipSettingsQuery(); - const memberships = membershipSettingsQuery.data?.memberships?.length - ? membershipSettingsQuery.data.memberships - : form.getValues('memberships'); + const memberships = membershipSettingsQuery.data?.length ? membershipSettingsQuery.data : form.getValues('plans'); const formData = form.watch(); @@ -38,7 +37,7 @@ function MembershipSettings() { useEffect(() => { if (membershipSettingsQuery.data) { - reset(membershipSettingsQuery.data); + reset({ plans: membershipSettingsQuery.data }); } }, [reset, membershipSettingsQuery.data]); @@ -46,29 +45,26 @@ function MembershipSettings() { return ; } + function handleNewMembershipClick() { + showModal({ + component: SubscriptionModal, + props: { + title: __('Create Membership', 'tutor'), + icon: , + }, + depthIndex: 9999, + }); + } + return (
- { - showModal({ - component: SubscriptionModal, - props: { - title: __('Create Membership', 'tutor'), - icon: , - }, - depthIndex: 9999, - }); - }} - /> - } - > - Membership list + }> + + + - +
); } diff --git a/assets/react/v3/entries/pro/membership-settings/components/fields/FormFeatureItem.tsx b/assets/react/v3/entries/pro/membership-settings/components/fields/FormFeatureItem.tsx index edc1687247..c472405927 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/fields/FormFeatureItem.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/fields/FormFeatureItem.tsx @@ -15,7 +15,7 @@ import { isRTL } from '@/v3/shared/config/constants'; import { __ } from '@wordpress/i18n'; import { useSortable } from '@dnd-kit/sortable'; -interface Feature { +export interface Feature { id: string; icon: string; content: string; @@ -44,7 +44,7 @@ export default function FormFeatureItem({ id, field, fieldState, handleDeleteCli }; function handleIconChange(icon: keyof typeof featureIcons) { - field.onChange({ ...field.value, icon: featureIcons[icon] }); + field.onChange({ ...field.value, icon: icon }); } function handleContentChange(content: string) { @@ -66,7 +66,7 @@ export default function FormFeatureItem({ id, field, fieldState, handleDeleteCli type="button" css={styles.iconSelector} onClick={() => setIsOpen(!isOpen)} - dangerouslySetInnerHTML={{ __html: field.value.icon }} + dangerouslySetInnerHTML={{ __html: featureIcons[field.value.icon as keyof typeof featureIcons] }} /> void; + plan?: MembershipPlan; } -export default function SubscriptionModal({ title, subtitle, icon, closeModal }: SubscriptionModalProps) { +export default function SubscriptionModal({ title, subtitle, icon, plan, closeModal }: SubscriptionModalProps) { const form = useFormWithGlobalError({ - defaultValues: { - plan_name: '', - short_description: '', - features: [], - categories: [], - payment_type: '', - plan_type: '', - recurring_value: '', - recurring_interval: '', - recurring_limit: '', - regular_price: '', - offer_sale_price: false, - sale_price: '', - schedule_sale_price: false, - sale_price_from: '', - sale_price_from_date: '', - sale_price_from_time: '', - sale_price_to: '', - sale_price_to_date: '', - sale_price_to_time: '', - charge_enrollment_fee: false, - enrollment_fee: '', - provide_certificate: false, - is_featured: false, - }, + defaultValues: plan ? convertPlanToFormData(plan) : defaultValues, }); + const saveMembershipPlanMutation = useSaveMembershipPlanMutation(); + + useEffect(() => { + console.log({ plan }); + if (plan) { + form.reset(convertPlanToFormData(plan)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [plan]); + + function handleSubmit() { + form.handleSubmit(async (data) => { + const payload = convertFormDataToPayload(data); + const response = await saveMembershipPlanMutation.mutateAsync(payload); + if (response.status_code === 200 || response.status_code === 201) { + closeModal({ action: 'CONFIRM' }); + } + })(); + } + const isFormDirty = form.formState.isDirty; const chargeEnrolmentFee = form.watch('charge_enrollment_fee'); const hasSale = form.watch('offer_sale_price'); - const regularPrice = form.watch(`regular_price`); - const hasScheduledSale = !!form.watch(`schedule_sale_price`); + const regularPrice = form.watch('regular_price'); + const hasScheduledSale = !!form.watch('schedule_sale_price'); + const isFeatured = !!form.watch('is_featured'); const lifetimePresets = [3, 6, 9, 12]; const lifetimeOptions = [ @@ -78,6 +86,12 @@ export default function SubscriptionModal({ title, subtitle, icon, closeModal }: }, ]; + const planType = form.watch('plan_type'); + const planTypeOptions = [ + { label: __('Full Site', 'tutor'), value: 'full_site' }, + { label: __('Specific Categories', 'tutor'), value: 'category' }, + ]; + return ( { - form.handleSubmit((data) => { - console.log(data); - })(); - }} + onClick={handleSubmit} + loading={saveMembershipPlanMutation.isPending} > {__('Save', 'tutor')} @@ -192,7 +203,7 @@ export default function SubscriptionModal({ title, subtitle, icon, closeModal }: render={(controllerProps) => (  
} + label={CURRENT_VIEWPORT.isAboveMobile ?
 
: __('Recurring Options', 'tutor')} options={[ { label: __('Day(s)', 'tutor'), value: 'day' }, { label: __('Week(s)', 'tutor'), value: 'week' }, @@ -237,12 +248,27 @@ export default function SubscriptionModal({ title, subtitle, icon, closeModal }: ( - + )} /> + + ( + + )} + /> + + ( )} @@ -296,6 +322,14 @@ export default function SubscriptionModal({ title, subtitle, icon, closeModal }: )} /> + + } + /> + +
', - crossCircle: + cross_circle: '', tick: '', cross: '', - plusSquare: + plus_square: '', - minusSquare: + minus_square: '', - plusCircle: + plus_circle: '', - minusCircle: + minus_circle: '', - tickCircleFill: + tick_circle_fill: '', - crossCircleFill: + cross_circle_fill: '', - plusCircleFill: + plus_circle_fill: '', - minusCircleFill: + minus_circle_fill: '', - plusSquareFill: + plus_square_fill: '', - minusSquareFill: + minus_square_fill: '', }; diff --git a/assets/react/v3/entries/pro/membership-settings/services/memberships.ts b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts index 7b76cb5bef..304d870f70 100644 --- a/assets/react/v3/entries/pro/membership-settings/services/memberships.ts +++ b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts @@ -1,24 +1,173 @@ -// import { wpAjaxInstance } from '@Utils/api'; -// import endpoints from '@Utils/endpoints'; -import { useQuery } from '@tanstack/react-query'; - -export interface Membership { - id: number; - name: string; - price: number; - duration: number; - duration_type: string; - description: string; - is_default: boolean; +import { useToast } from '@/v3/shared/atoms/Toast'; +import { DateFormats } from '@/v3/shared/config/constants'; +import { type ErrorResponse } from '@/v3/shared/utils/form'; +import { type TutorMutationResponse } from '@/v3/shared/utils/types'; +import { convertGMTtoLocalDate, convertToErrorMessage, convertToGMT } from '@/v3/shared/utils/util'; +import { wpAjaxInstance } from '@Utils/api'; +import endpoints from '@Utils/endpoints'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { format } from 'date-fns'; +import { type Feature } from '../components/fields/FormFeatureItem'; + +export interface MembershipPlan { + id: string; + is_enabled: '0' | '1'; + plan_name: string | null; + plan_order: string; + plan_type: 'full_site' | 'category'; + categories: number[]; + payment_type: string; + short_description: string | null; + description: string | null; + provide_certificate: '0' | '1'; + recurring_interval: string; + recurring_limit: string; + recurring_value: string; + regular_price: string; + sale_price: string | null; + sale_price_from: string | null; + sale_price_to: string | null; + enrollment_fee: string | null; + is_featured: '0' | '1'; + featured_text: string | null; } export interface MembershipSettings { - memberships: Membership[]; + plans: MembershipPlan[]; +} + +export interface MembershipFormData { + id?: string; + plan_name: string; + plan_type: 'full_site' | 'category'; + short_description: string; + features: Feature[]; + categories: number[]; + recurring_value: string; + recurring_interval: string; + recurring_limit: string; + regular_price: string; + offer_sale_price: boolean; + sale_price: string; + schedule_sale_price: boolean; + sale_price_from_date: string; + sale_price_from_time: string; + sale_price_to_date: string; + sale_price_to_time: string; + charge_enrollment_fee: boolean; + enrollment_fee: string; + do_not_provide_certificate: boolean; + is_featured: boolean; + featured_text: string; } +export const defaultValues: MembershipFormData = { + plan_name: '', + plan_type: 'full_site', + short_description: '', + features: [], + categories: [], + recurring_value: '', + recurring_interval: '', + recurring_limit: '', + regular_price: '', + offer_sale_price: false, + sale_price: '', + schedule_sale_price: false, + sale_price_from_date: '', + sale_price_from_time: '', + sale_price_to_date: '', + sale_price_to_time: '', + charge_enrollment_fee: false, + enrollment_fee: '', + do_not_provide_certificate: false, + is_featured: false, + featured_text: '', +}; + +export type DurationUnit = 'hour' | 'day' | 'week' | 'month' | 'year'; + +export type MembershipPayload = { + id?: string; // only for update + plan_name: string; + short_description: string; + description: string; + plan_type: 'category' | 'full_site'; + cat_ids?: number[]; + recurring_value: string; + recurring_interval: Omit; + recurring_limit: string; // 0 for until canceled + regular_price: string; + sale_price?: string; + sale_price_from?: string; // start date + sale_price_to?: string; // end date + enrollment_fee?: string; + provide_certificate: '0' | '1'; + is_featured: '0' | '1'; + featured_text: string; +}; + +export const convertPlanToFormData = (plan: MembershipPlan): MembershipFormData => { + return { + id: plan.id, + plan_name: plan.plan_name ?? '', + short_description: plan.short_description ?? '', + regular_price: plan.regular_price ?? '0', + plan_type: plan.plan_type ?? 'course', + categories: plan.categories ?? [], + recurring_value: plan.recurring_value ?? '0', + recurring_interval: plan.recurring_interval ?? 'month', + recurring_limit: plan.recurring_limit === '0' ? 'Until cancelled' : plan.recurring_limit || '', + features: plan.description ? (JSON.parse(plan.description) as Feature[]) : [], + charge_enrollment_fee: !!Number(plan.enrollment_fee), + enrollment_fee: plan.enrollment_fee ?? '0', + do_not_provide_certificate: !Number(plan.provide_certificate), + is_featured: !!Number(plan.is_featured), + featured_text: plan.featured_text ?? '', + offer_sale_price: !!Number(plan.sale_price), + sale_price: plan.sale_price ?? '0', + schedule_sale_price: !!plan.sale_price_from, + sale_price_from_date: plan.sale_price_from + ? format(convertGMTtoLocalDate(plan.sale_price_from), DateFormats.yearMonthDay) + : '', + sale_price_from_time: plan.sale_price_from + ? format(convertGMTtoLocalDate(plan.sale_price_from), DateFormats.hoursMinutes) + : '', + sale_price_to_date: plan.sale_price_to + ? format(convertGMTtoLocalDate(plan.sale_price_to), DateFormats.yearMonthDay) + : '', + sale_price_to_time: plan.sale_price_to + ? format(convertGMTtoLocalDate(plan.sale_price_to), DateFormats.hoursMinutes) + : '', + }; +}; + +export const convertFormDataToPayload = (formData: MembershipFormData): MembershipPayload => { + return { + ...(formData.id && String(formData.id) !== '0' && { id: formData.id }), + plan_name: formData.plan_name, + short_description: formData.short_description, + description: JSON.stringify(formData.features), + plan_type: formData.plan_type, + ...(formData.plan_type === 'category' && { cat_ids: formData.categories }), + regular_price: formData.regular_price, + recurring_value: formData.recurring_value, + recurring_interval: formData.recurring_interval, + recurring_limit: formData.recurring_limit === 'Until cancelled' ? '0' : formData.recurring_limit, + is_featured: formData.is_featured ? '1' : '0', + featured_text: formData.featured_text, + ...(formData.charge_enrollment_fee && { enrollment_fee: formData.enrollment_fee }), + sale_price: formData.offer_sale_price ? formData.sale_price : '0', + ...(formData.schedule_sale_price && { + sale_price_from: convertToGMT(new Date(`${formData.sale_price_from_date} ${formData.sale_price_from_time}`)), + sale_price_to: convertToGMT(new Date(`${formData.sale_price_to_date} ${formData.sale_price_to_time}`)), + }), + provide_certificate: formData.do_not_provide_certificate ? '0' : '1', + }; +}; + const getMembershipSettings = () => { - return Promise.resolve(null); - // return wpAjaxInstance.get(endpoints.GET_MEMBERSHIP_SETTINGS).then((response) => response.data); + return wpAjaxInstance.get(endpoints.GET_MEMBERSHIP_PLANS).then((response) => response.data); }; export const useMembershipSettingsQuery = () => { @@ -27,3 +176,34 @@ export const useMembershipSettingsQuery = () => { queryFn: getMembershipSettings, }); }; + +const saveMembershipPlan = (payload: MembershipPayload) => { + return wpAjaxInstance.post>(endpoints.SAVE_MEMBERSHIP_PLAN, { + ...(payload.id && { id: payload.id }), + ...payload, + }); +}; + +export const useSaveMembershipPlanMutation = () => { + const queryClient = useQueryClient(); + const { showToast } = useToast(); + + return useMutation({ + mutationFn: saveMembershipPlan, + onSuccess: (response) => { + if (response.status_code === 200 || response.status_code === 201) { + showToast({ + message: response.message, + type: 'success', + }); + + queryClient.invalidateQueries({ + queryKey: ['MembershipSettings'], + }); + } + }, + onError: (error: ErrorResponse) => { + showToast({ type: 'danger', message: convertToErrorMessage(error) }); + }, + }); +}; diff --git a/assets/react/v3/shared/utils/endpoints.ts b/assets/react/v3/shared/utils/endpoints.ts index fcb293b033..9ce9ab9269 100644 --- a/assets/react/v3/shared/utils/endpoints.ts +++ b/assets/react/v3/shared/utils/endpoints.ts @@ -102,6 +102,11 @@ const endpoints = { CREATE_ENROLLMENT: 'tutor_enroll_bulk_student', GET_COURSE_BUNDLE_LIST: 'tutor_course_bundle_list', GET_UNENROLLED_USERS: 'tutor_unenrolled_users', + + // MEMBERSHIP + GET_MEMBERSHIP_PLANS: 'tutor_membership_plans', + SAVE_MEMBERSHIP_PLAN: 'tutor_membership_plan_save', + DELETE_MEMBERSHIP_PLAN: 'tutor_membership_plan_delete', }; export default endpoints; diff --git a/assets/react/v3/shared/utils/types.ts b/assets/react/v3/shared/utils/types.ts index 888ea277de..b5b5ecb975 100644 --- a/assets/react/v3/shared/utils/types.ts +++ b/assets/react/v3/shared/utils/types.ts @@ -6,7 +6,7 @@ export type CourseProgressSteps = 'basic' | 'curriculum' | 'additional' | 'certi export type IconCollection = keyof typeof collection; -export const localHasOwnProperty = (obj: T, key: PropertyKey): key is keyof T => { +export const localHasOwnProperty = (obj: T, key: PropertyKey): key is keyof T => { return key in obj; }; @@ -117,3 +117,9 @@ export interface WPResponse { message: string; status_code: AxiosResponse['status']; } + +export interface TutorMutationResponse { + data: T; + message: string; + status_code: number; +} From 9ed2a90b933f8b2b1cd3c5b07a2a4eca2e0fd5dc Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Mon, 30 Dec 2024 17:12:22 +0600 Subject: [PATCH 07/18] SubscriptionModal renamed to MembershipModal --- .../pro/membership-settings/components/MembershipItem.tsx | 4 ++-- .../pro/membership-settings/components/MembershipSettings.tsx | 4 ++-- .../modals/{SubscriptionModal.tsx => MembershipModal.tsx} | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename assets/react/v3/entries/pro/membership-settings/components/modals/{SubscriptionModal.tsx => MembershipModal.tsx} (99%) diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx index 071bebf164..b5b5e8e725 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx @@ -1,6 +1,6 @@ import Button from '@/v3/shared/atoms/Button'; import { type MembershipPlan } from '../services/memberships'; -import SubscriptionModal from './modals/SubscriptionModal'; +import MembershipModal from './modals/MembershipModal'; import { __ } from '@wordpress/i18n'; import SVGIcon from '@/v3/shared/atoms/SVGIcon'; import { useModal } from '@/v3/shared/components/modals/Modal'; @@ -18,7 +18,7 @@ export default function MembershipItem({ data }: MembershipItemProps) { +
+ +
+ +
+
+ {data.plan_name}
${data.regular_price} per month
+
+

Renews every month | Certificate available | Length

+
+
+
+ } + /> + { + setIsOpen(true); + }} + closePopover={() => setIsOpen(false)} + > + } + onClick={() => { + showModal({ + component: MembershipModal, + props: { + title: __('Update Membership', 'tutor'), + icon: , + plan: data, + }, + depthIndex: 9999, + }); + }} + onClosePopover={() => setIsOpen(false)} + /> + } + isTrash={true} + onClick={() => { + console.log('Delete!'); + }} + onClosePopover={() => setIsOpen(false)} + /> + +
); } + +const styles = { + wrapper: css` + background-color: ${colorTokens.background.white}; + padding: ${spacing[16]} ${spacing[24]}; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + + &:hover { + [data-drag-button] { + display: block; + } + } + + &:not(:last-of-type) { + border-bottom: 1px solid ${colorTokens.stroke.divider}; + } + + &:first-of-type { + border-top-left-radius: ${borderRadius[6]}; + border-top-right-radius: ${borderRadius[6]}; + } + + &:last-of-type { + border-bottom-left-radius: ${borderRadius[6]}; + border-bottom-right-radius: ${borderRadius[6]}; + } + `, + content: css` + display: flex; + align-items: center; + gap: ${spacing[12]}; + `, + planInfo: css` + display: flex; + flex-direction: column; + gap: ${spacing[6]}; + `, + planTitle: css` + display: flex; + align-items: center; + gap: ${spacing[8]}; + + font-size: ${fontSize[16]}; + line-height: ${lineHeight[20]}; + font-weight: ${fontWeight.regular}; + color: ${colorTokens.text.primary}; + + strong { + font-weight: ${fontWeight.medium}; + } + + span { + height: 2px; + width: 2px; + border-radius: ${borderRadius.circle}; + background-color: ${colorTokens.icon.default}; + } + `, + planFeatures: css` + font-size: ${fontSize[11]}; + line-height: ${lineHeight[16]}; + color: ${colorTokens.text.hints}; + `, + actions: css` + display: flex; + align-items: center; + gap: ${spacing[16]}; + `, + dragButton: css` + display: flex; + align-items: center; + padding: 0; + color: ${colorTokens.icon.default}; + background: transparent; + border: none; + cursor: grab; + + position: absolute; + height: 100%; + left: -${spacing[24]}; + top: 0; + display: none; + + :focus-visible { + border-radius: ${borderRadius[4]}; + outline: 2px solid ${colorTokens.stroke.brand}; + } + `, +}; diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx index 5350539582..cb0960d925 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx @@ -2,9 +2,14 @@ import Button from '@/v3/shared/atoms/Button'; import SVGIcon from '@/v3/shared/atoms/SVGIcon'; import For from '@/v3/shared/controls/For'; import { __ } from '@wordpress/i18n'; -import { useFormContext } from 'react-hook-form'; +import { useFieldArray, useFormContext } from 'react-hook-form'; import MembershipItem from './MembershipItem'; import { type MembershipSettings } from '../services/memberships'; +import { css } from '@emotion/react'; +import { colorTokens, spacing } from '@/v3/shared/config/styles'; +import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { restrictToParentElement } from '@dnd-kit/modifiers'; interface MembershipListProps { onNewMembershipClick: () => void; @@ -12,25 +17,74 @@ interface MembershipListProps { export default function MembershipList({ onNewMembershipClick }: MembershipListProps) { const form = useFormContext(); - const membershipPlans = form.watch('plans'); - console.log(form.getValues()); + const { fields, move } = useFieldArray({ + control: form.control, + name: 'plans', + keyName: '_id', + }); + + console.log(fields); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); return ( -
+
- {(item) => } + { + const { active, over } = event; + if (!over) { + return; + } + + if (active.id !== over.id) { + const activeIndex = fields.findIndex((item) => item.id === active.id); + const overIndex = fields.findIndex((item) => item.id === over.id); + + move(activeIndex, overIndex); + } + }} + > +
+ + {(item, idx) => } + +
+
- +
+ +
); } + +const styles = { + wrapper: css` + display: flex; + flex-direction: column; + gap: ${spacing[16]}; + `, + membershipList: css` + background-color: ${colorTokens.background.white}; + border: 1px solid ${colorTokens.stroke.divider}; + border-radius: ${spacing[6]}; + `, +}; diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx index eeee85788d..13db1457cf 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipSettings.tsx @@ -37,7 +37,11 @@ function MembershipSettings() { useEffect(() => { if (membershipSettingsQuery.data) { - reset({ plans: membershipSettingsQuery.data }); + const updatedPlans = membershipSettingsQuery.data?.map((item) => ({ + ...item, + is_enabled: !!Number(item.is_enabled), + })); + reset({ plans: updatedPlans }); } }, [reset, membershipSettingsQuery.data]); @@ -64,7 +68,14 @@ function MembershipSettings() { - + ({ id, plan_name, is_enabled })), + })} + />
); } diff --git a/assets/react/v3/entries/pro/membership-settings/services/memberships.ts b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts index 304d870f70..8baca5f8b4 100644 --- a/assets/react/v3/entries/pro/membership-settings/services/memberships.ts +++ b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts @@ -11,7 +11,7 @@ import { type Feature } from '../components/fields/FormFeatureItem'; export interface MembershipPlan { id: string; - is_enabled: '0' | '1'; + is_enabled: boolean; plan_name: string | null; plan_order: string; plan_type: 'full_site' | 'category'; From 8a1a4e8d0509edd292301a3bbbc1bc7a83ee357e Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Tue, 31 Dec 2024 10:24:53 +0600 Subject: [PATCH 09/18] Delete membership api integration added --- .../components/MembershipItem.tsx | 22 +++++++++++++-- .../services/memberships.ts | 28 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx index a9c77205f7..fcda13c664 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx @@ -1,4 +1,4 @@ -import { type MembershipSettings, type MembershipPlan } from '../services/memberships'; +import { type MembershipSettings, type MembershipPlan, useDeleteMembershipPlanMutation } from '../services/memberships'; import MembershipModal from './modals/MembershipModal'; import { __ } from '@wordpress/i18n'; import SVGIcon from '@/v3/shared/atoms/SVGIcon'; @@ -12,6 +12,7 @@ import { useState } from 'react'; import { useSortable } from '@dnd-kit/sortable'; import { animateLayoutChanges } from '@/v3/shared/utils/dndkit'; import { CSS } from '@dnd-kit/utilities'; +import StaticConfirmationModal from '@/v3/shared/components/modals/StaticConfirmationModal'; interface MembershipItemProps { data: MembershipPlan; @@ -29,6 +30,8 @@ export default function MembershipItem({ data, index }: MembershipItemProps) { animateLayoutChanges, }); + const deleteMembershipPlanMutation = useDeleteMembershipPlanMutation(); + const style = { transform: CSS.Transform.toString(transform ? { ...transform, scaleX: 1, scaleY: 1 } : null), transition, @@ -40,6 +43,7 @@ export default function MembershipItem({ data, index }: MembershipItemProps) { +
@@ -49,6 +53,7 @@ export default function MembershipItem({ data, index }: MembershipItemProps) {

Renews every month | Certificate available | Length

+
} isTrash={true} - onClick={() => { - console.log('Delete!'); + onClick={async () => { + const { action } = await showModal({ + component: StaticConfirmationModal, + props: { + title: __('Are you sure to delete this?', 'tutor'), + icon: , + }, + depthIndex: 9999, + }); + + if (action === 'CONFIRM') { + deleteMembershipPlanMutation.mutate(data.id); + } }} onClosePopover={() => setIsOpen(false)} /> diff --git a/assets/react/v3/entries/pro/membership-settings/services/memberships.ts b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts index 8baca5f8b4..c37dd22f92 100644 --- a/assets/react/v3/entries/pro/membership-settings/services/memberships.ts +++ b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts @@ -207,3 +207,31 @@ export const useSaveMembershipPlanMutation = () => { }, }); }; + +const deleteMembershipPlan = (id: string) => { + return wpAjaxInstance.post>(endpoints.DELETE_MEMBERSHIP_PLAN, { id }); +}; + +export const useDeleteMembershipPlanMutation = () => { + const queryClient = useQueryClient(); + const { showToast } = useToast(); + + return useMutation({ + mutationFn: deleteMembershipPlan, + onSuccess: (response) => { + if (response.status_code === 200) { + showToast({ + message: response.message, + type: 'success', + }); + + queryClient.invalidateQueries({ + queryKey: ['MembershipSettings'], + }); + } + }, + onError: (error: ErrorResponse) => { + showToast({ type: 'danger', message: convertToErrorMessage(error) }); + }, + }); +}; From 2df9f58cb5e1f0842fb628fc79cda6afb3535bb2 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Tue, 31 Dec 2024 12:36:23 +0600 Subject: [PATCH 10/18] Membership info and duplicate functions added --- .../components/MembershipItem.tsx | 85 ++++++++++++++++++- .../components/MembershipList.tsx | 2 - .../components/modals/MembershipModal.tsx | 1 - .../services/memberships.ts | 28 ++++++ assets/react/v3/shared/utils/endpoints.ts | 1 + 5 files changed, 110 insertions(+), 7 deletions(-) diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx index fcda13c664..198334d381 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx @@ -1,6 +1,12 @@ -import { type MembershipSettings, type MembershipPlan, useDeleteMembershipPlanMutation } from '../services/memberships'; +import { + type MembershipSettings, + type MembershipPlan, + useDeleteMembershipPlanMutation, + type DurationUnit, + useDuplicateMembershipPlanMutation, +} from '../services/memberships'; import MembershipModal from './modals/MembershipModal'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import SVGIcon from '@/v3/shared/atoms/SVGIcon'; import { useModal } from '@/v3/shared/components/modals/Modal'; import { css } from '@emotion/react'; @@ -13,12 +19,33 @@ import { useSortable } from '@dnd-kit/sortable'; import { animateLayoutChanges } from '@/v3/shared/utils/dndkit'; import { CSS } from '@dnd-kit/utilities'; import StaticConfirmationModal from '@/v3/shared/components/modals/StaticConfirmationModal'; +import Show from '@/v3/shared/controls/Show'; +import { formatPrice } from '@/v3/shared/utils/currency'; +import { makeFirstCharacterUpperCase } from '@/v3/shared/utils/util'; +import { AnimationType } from '@/v3/shared/hooks/useAnimation'; interface MembershipItemProps { data: MembershipPlan; index: number; } +function formatRepeatUnit(unit: Omit, value: number) { + switch (unit) { + case 'hour': + return value > 1 ? __('Hours', 'tutor') : __('Hour', 'tutor'); + case 'day': + return value > 1 ? __('Days', 'tutor') : __('Day', 'tutor'); + case 'week': + return value > 1 ? __('Weeks', 'tutor') : __('Week', 'tutor'); + case 'month': + return value > 1 ? __('Months', 'tutor') : __('Month', 'tutor'); + case 'year': + return value > 1 ? __('Years', 'tutor') : __('Year', 'tutor'); + case 'until_cancellation': + return __('Until Cancellation', 'tutor'); + } +} + export default function MembershipItem({ data, index }: MembershipItemProps) { const form = useFormContext(); const { showModal } = useModal(); @@ -30,6 +57,7 @@ export default function MembershipItem({ data, index }: MembershipItemProps) { animateLayoutChanges, }); + const duplicateMembershipPlanMutation = useDuplicateMembershipPlanMutation(); const deleteMembershipPlanMutation = useDeleteMembershipPlanMutation(); const style = { @@ -48,9 +76,41 @@ export default function MembershipItem({ data, index }: MembershipItemProps) {
- {data.plan_name}
${data.regular_price} per month
+ {data.plan_name} + +
+ {sprintf( + __('%s per %s', 'tutor'), + formatPrice(Number(data.regular_price)), + makeFirstCharacterUpperCase(data.recurring_interval), + )} +
-

Renews every month | Certificate available | Length

+

+ + {sprintf( + __('Renews every %s %s', 'tutor'), + data.recurring_value.toString().padStart(2, '0'), + formatRepeatUnit(data.recurring_interval, Number(data.recurring_value)), + )} + + + | + {__('Certificate available', 'tutor')} + + + | + {sprintf(__('%s Times', 'tutor'), data.recurring_limit.toString().padStart(2, '0'))} + + } + > + | + {__('Until Cancellation', 'tutor')} + +

@@ -62,6 +122,7 @@ export default function MembershipItem({ data, index }: MembershipItemProps) { /> { setIsOpen(true); @@ -84,6 +145,14 @@ export default function MembershipItem({ data, index }: MembershipItemProps) { }} onClosePopover={() => setIsOpen(false)} /> + } + onClick={() => { + duplicateMembershipPlanMutation.mutate(data.id); + }} + onClosePopover={() => setIsOpen(false)} + /> } @@ -170,6 +239,9 @@ const styles = { background-color: ${colorTokens.icon.default}; } `, + planPerMonth: css` + color: ${colorTokens.text.title}; + `, planFeatures: css` font-size: ${fontSize[11]}; line-height: ${lineHeight[16]}; @@ -200,4 +272,9 @@ const styles = { outline: 2px solid ${colorTokens.stroke.brand}; } `, + pipe: css` + display: inline-block; + color: ${colorTokens.stroke.divider}; + padding-inline: ${spacing[8]}; + `, }; diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx index cb0960d925..c9186b5782 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipList.tsx @@ -24,8 +24,6 @@ export default function MembershipList({ onNewMembershipClick }: MembershipListP keyName: '_id', }); - console.log(fields); - const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { diff --git a/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx b/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx index 69b7b454bb..4ee78f0295 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx @@ -49,7 +49,6 @@ export default function MembershipModal({ title, subtitle, icon, plan, closeModa const saveMembershipPlanMutation = useSaveMembershipPlanMutation(); useEffect(() => { - console.log({ plan }); if (plan) { form.reset(convertPlanToFormData(plan)); } diff --git a/assets/react/v3/entries/pro/membership-settings/services/memberships.ts b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts index c37dd22f92..74632d092e 100644 --- a/assets/react/v3/entries/pro/membership-settings/services/memberships.ts +++ b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts @@ -208,6 +208,34 @@ export const useSaveMembershipPlanMutation = () => { }); }; +const duplicateMembershipPlan = (id: string) => { + return wpAjaxInstance.post>(endpoints.DUPLICATE_MEMBERSHIP_PLAN, { id }); +}; + +export const useDuplicateMembershipPlanMutation = () => { + const queryClient = useQueryClient(); + const { showToast } = useToast(); + + return useMutation({ + mutationFn: duplicateMembershipPlan, + onSuccess: (response) => { + if (response.status_code === 201) { + showToast({ + message: response.message, + type: 'success', + }); + + queryClient.invalidateQueries({ + queryKey: ['MembershipSettings'], + }); + } + }, + onError: (error: ErrorResponse) => { + showToast({ type: 'danger', message: convertToErrorMessage(error) }); + }, + }); +}; + const deleteMembershipPlan = (id: string) => { return wpAjaxInstance.post>(endpoints.DELETE_MEMBERSHIP_PLAN, { id }); }; diff --git a/assets/react/v3/shared/utils/endpoints.ts b/assets/react/v3/shared/utils/endpoints.ts index 9ce9ab9269..19aab5d376 100644 --- a/assets/react/v3/shared/utils/endpoints.ts +++ b/assets/react/v3/shared/utils/endpoints.ts @@ -106,6 +106,7 @@ const endpoints = { // MEMBERSHIP GET_MEMBERSHIP_PLANS: 'tutor_membership_plans', SAVE_MEMBERSHIP_PLAN: 'tutor_membership_plan_save', + DUPLICATE_MEMBERSHIP_PLAN: 'tutor_membership_plan_duplicate', DELETE_MEMBERSHIP_PLAN: 'tutor_membership_plan_delete', }; From 5b8085743b2b056c0fca8bdbc7241cd3139da979 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Tue, 31 Dec 2024 17:12:48 +0600 Subject: [PATCH 11/18] Category selection option added --- .../membership-settings/components/App.tsx | 22 ++- .../components/Categories.tsx | 85 ++++++++ .../components/CategoryItem.tsx | 67 +++++++ .../components/IconsAndFeatures.tsx | 6 +- .../components/modals/MembershipModal.tsx | 11 +- .../entries/pro/membership-settings/index.tsx | 9 +- .../services/memberships.ts | 13 +- .../CategoryListTable.tsx | 147 ++++++++++++++ .../CourseListTable.tsx | 185 ++++++++++++++++++ .../CourseCategorySelectModal/SearchField.tsx | 44 +++++ .../CourseCategorySelectModal/index.tsx | 62 ++++++ assets/react/v3/shared/config/icon-list.ts | 12 +- .../v3/shared/services/course_category.ts | 45 +++++ 13 files changed, 674 insertions(+), 34 deletions(-) create mode 100644 assets/react/v3/entries/pro/membership-settings/components/Categories.tsx create mode 100644 assets/react/v3/entries/pro/membership-settings/components/CategoryItem.tsx create mode 100644 assets/react/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx create mode 100644 assets/react/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx create mode 100644 assets/react/v3/shared/components/modals/CourseCategorySelectModal/SearchField.tsx create mode 100644 assets/react/v3/shared/components/modals/CourseCategorySelectModal/index.tsx create mode 100644 assets/react/v3/shared/services/course_category.ts diff --git a/assets/react/v3/entries/pro/membership-settings/components/App.tsx b/assets/react/v3/entries/pro/membership-settings/components/App.tsx index 837ecadce1..693222deae 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/App.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/App.tsx @@ -1,6 +1,8 @@ +import { useState } from 'react'; import { Global } from '@emotion/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { useState } from 'react'; +import { QueryParamProvider } from 'use-query-params'; +import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'; import ToastProvider from '@Atoms/Toast'; @@ -30,14 +32,16 @@ function App() { return ( - - - - - - - - + + + + + + + + + + ); } diff --git a/assets/react/v3/entries/pro/membership-settings/components/Categories.tsx b/assets/react/v3/entries/pro/membership-settings/components/Categories.tsx new file mode 100644 index 0000000000..d29568c433 --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/components/Categories.tsx @@ -0,0 +1,85 @@ +import Show from '@/v3/shared/controls/Show'; +import { type MembershipFormData } from '../services/memberships'; +import { type FormWithGlobalErrorType } from '@/v3/shared/hooks/useFormWithGlobalError'; +import For from '@/v3/shared/controls/For'; +import CategoryItem from './CategoryItem'; +import { __, sprintf } from '@wordpress/i18n'; +import SVGIcon from '@/v3/shared/atoms/SVGIcon'; +import { useModal } from '@/v3/shared/components/modals/Modal'; +import Button from '@/v3/shared/atoms/Button'; +import CourseCategorySelectModal from '@/v3/shared/components/modals/CourseCategorySelectModal'; +import { css } from '@emotion/react'; +import { borderRadius, colorTokens } from '@/v3/shared/config/styles'; + +interface CategoriesProps { + form: FormWithGlobalErrorType; +} + +export default function Categories({ form }: CategoriesProps) { + const { showModal } = useModal(); + const categories = form.watch('categories'); + + return ( + <> + +
+ + {(category) => ( + { + form.setValue( + 'categories', + categories.filter((item) => item.id !== category.id), + { shouldDirty: true }, + ); + }} + /> + )} + +
+
+ + + + ); +} + +const styles = { + categoriesWrapper: css` + background-color: ${colorTokens.background.white}; + border: 1px solid ${colorTokens.stroke.divider}; + border-radius: ${borderRadius[6]}; + `, + addCategoriesButton: css` + width: fit-content; + background-color: ${colorTokens.background.white}; + color: ${colorTokens.text.brand}; + + svg, + :active svg { + color: ${colorTokens.text.brand} !important; + } + `, +}; diff --git a/assets/react/v3/entries/pro/membership-settings/components/CategoryItem.tsx b/assets/react/v3/entries/pro/membership-settings/components/CategoryItem.tsx new file mode 100644 index 0000000000..395ad97860 --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/components/CategoryItem.tsx @@ -0,0 +1,67 @@ +import Button from '@/v3/shared/atoms/Button'; +import SVGIcon from '@/v3/shared/atoms/SVGIcon'; +import { borderRadius, colorTokens, spacing } from '@/v3/shared/config/styles'; +import { typography } from '@/v3/shared/config/typography'; +import { css } from '@emotion/react'; +import { type ReactNode } from 'react'; +import coursePlaceholder from '@Images/course-placeholder.png'; + +interface CategoryItemProps { + image: string; + title: string; + subTitle: string | ReactNode; + handleDeleteClick: () => void; +} + +export default function CategoryItem({ image, title, subTitle, handleDeleteClick }: CategoryItemProps) { + return ( +
+
+ course item +
+
+
{title}
+
{subTitle}
+
+
+ +
+
+ ); +} + +const styles = { + selectedItem: css` + padding: ${spacing[12]}; + display: flex; + align-items: center; + gap: ${spacing[16]}; + + &:not(:last-child) { + border-bottom: 1px solid ${colorTokens.stroke.divider}; + } + `, + selectedContent: css` + width: 100%; + `, + selectedTitle: css` + ${typography.small()}; + color: ${colorTokens.text.primary}; + margin-bottom: ${spacing[4]}; + `, + selectedSubTitle: css` + ${typography.small()}; + color: ${colorTokens.text.hints}; + `, + selectedThumb: css` + height: 48px; + `, + thumbnail: css` + width: 48px; + height: 48px; + border-radius: ${borderRadius[4]}; + object-fit: cover; + `, +}; diff --git a/assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx b/assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx index 660f0f477a..dca0d3157c 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx @@ -6,7 +6,6 @@ import For from '@/v3/shared/controls/For'; import Show from '@/v3/shared/controls/Show'; import { css } from '@emotion/react'; import { __ } from '@wordpress/i18n'; -import { featureIcons } from '../config/constants'; import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; import FormFeatureItem from './fields/FormFeatureItem'; import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable'; @@ -32,10 +31,7 @@ export default function IconsAndFeatures() {
-
diff --git a/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx b/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx index 4ee78f0295..ca91326632 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx @@ -5,7 +5,7 @@ import { Controller, FormProvider } from 'react-hook-form'; import Button from '@Atoms/Button'; import SVGIcon from '@Atoms/SVGIcon'; -import type { ModalProps } from '@Components/modals/Modal'; +import { type ModalProps } from '@Components/modals/Modal'; import ModalWrapper from '@Components/modals/ModalWrapper'; import { borderRadius, Breakpoint, colorTokens, spacing } from '@Config/styles'; @@ -34,6 +34,7 @@ import { } from '../../services/memberships'; import { useEffect } from 'react'; import FormRadioGroup from '@/v3/shared/components/fields/FormRadioGroup'; +import Categories from '../Categories'; const { tutor_currency } = tutorConfig; interface MembershipModalProps extends ModalProps { @@ -259,13 +260,7 @@ export default function MembershipModal({ title, subtitle, icon, plan, closeModa /> - ( - - )} - /> + diff --git a/assets/react/v3/entries/pro/membership-settings/index.tsx b/assets/react/v3/entries/pro/membership-settings/index.tsx index 252c76dfd8..71ce68ac49 100644 --- a/assets/react/v3/entries/pro/membership-settings/index.tsx +++ b/assets/react/v3/entries/pro/membership-settings/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; +import { HashRouter } from 'react-router-dom'; import ErrorBoundary from '@Components/ErrorBoundary'; import App from './components/App'; @@ -9,9 +10,11 @@ if (element) { const root = createRoot(element as HTMLElement); root.render( - - - + + + + + , ); } else { diff --git a/assets/react/v3/entries/pro/membership-settings/services/memberships.ts b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts index 74632d092e..3266075db4 100644 --- a/assets/react/v3/entries/pro/membership-settings/services/memberships.ts +++ b/assets/react/v3/entries/pro/membership-settings/services/memberships.ts @@ -9,13 +9,20 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { format } from 'date-fns'; import { type Feature } from '../components/fields/FormFeatureItem'; +export interface Category { + id: number; + title: string; + image: string; + total_courses: string; +} + export interface MembershipPlan { id: string; is_enabled: boolean; plan_name: string | null; plan_order: string; plan_type: 'full_site' | 'category'; - categories: number[]; + categories: Category[]; payment_type: string; short_description: string | null; description: string | null; @@ -42,7 +49,7 @@ export interface MembershipFormData { plan_type: 'full_site' | 'category'; short_description: string; features: Feature[]; - categories: number[]; + categories: Category[]; recurring_value: string; recurring_interval: string; recurring_limit: string; @@ -149,7 +156,7 @@ export const convertFormDataToPayload = (formData: MembershipFormData): Membersh short_description: formData.short_description, description: JSON.stringify(formData.features), plan_type: formData.plan_type, - ...(formData.plan_type === 'category' && { cat_ids: formData.categories }), + ...(formData.plan_type === 'category' && { cat_ids: formData.categories.map((item) => item.id) }), regular_price: formData.regular_price, recurring_value: formData.recurring_value, recurring_interval: formData.recurring_interval, diff --git a/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx new file mode 100644 index 0000000000..b80fc0cadc --- /dev/null +++ b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx @@ -0,0 +1,147 @@ +import Checkbox from '@Atoms/CheckBox'; +import { LoadingSection } from '@Atoms/LoadingSpinner'; +import { borderRadius, spacing } from '@Config/styles'; +import { typography } from '@Config/typography'; +import { usePaginatedTable } from '@Hooks/usePaginatedTable'; +import Paginator from '@Molecules/Paginator'; +import Table, { type Column } from '@Molecules/Table'; +import { css } from '@emotion/react'; + +import coursePlaceholder from '@Images/course-placeholder.png'; +import { __ } from '@wordpress/i18n'; +import type { UseFormReturn } from 'react-hook-form'; +import SearchField from './SearchField'; +import { useCourseCategoryQuery, type Category } from '@/v3/shared/services/course_category'; + +interface CategoryListTableProps { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + form: UseFormReturn; +} + +const CategoryListTable = ({ form }: CategoryListTableProps) => { + const categoryList: Category[] = form.watch('categories') ?? []; + const { pageInfo, onPageChange, itemsPerPage, offset, onFilterItems } = usePaginatedTable({ + updateQueryParams: false, + }); + const categoryListQuery = useCourseCategoryQuery({ + type: 'specific_category', + offset, + limit: itemsPerPage, + filter: pageInfo.filter, + }); + + function toggleSelection(isChecked = false) { + form.setValue('categories', isChecked ? (categoryListQuery.data?.results as Category[]) : []); + } + + function handleAllIsChecked() { + return ( + categoryList.length === categoryListQuery.data?.results.length && + categoryList?.every((item) => categoryListQuery.data?.results?.map((result) => result.id).includes(item.id)) + ); + } + + const columns: Column[] = [ + { + Header: categoryListQuery.data?.results.length ? ( + + ) : ( + __('Category', 'tutor') + ), + Cell: (item) => { + return ( +
+ { + const filteredItems = categoryList.filter((category) => category.id !== item.id); + const isNewItem = filteredItems?.length === categoryList.length; + + if (isNewItem) { + form.setValue('categories', [...filteredItems, item]); + } else { + form.setValue('categories', filteredItems); + } + }} + checked={categoryList.map((category) => category.id).includes(item.id)} + /> + {__('category +
+
{item.title}
+

{`${item.total_courses} ${__('Courses', 'tutor')}`}

+
+
+ ); + }, + width: 720, + }, + ]; + + if (categoryListQuery.isLoading) { + return ; + } + + if (!categoryListQuery.data) { + return
{__('Something went wrong', 'tutor')}
; + } + + return ( + <> +
+ +
+ +
+ + + +
+ +
+ + ); +}; + +export default CategoryListTable; + +const styles = { + tableActions: css` + padding: ${spacing[20]}; + `, + tableWrapper: css` + max-height: calc(100vh - 350px); + overflow: auto; + `, + paginatorWrapper: css` + margin: ${spacing[20]} ${spacing[16]}; + `, + checkboxWrapper: css` + display: flex; + align-items: center; + gap: ${spacing[12]}; + `, + courseItem: css` + ${typography.caption()}; + margin-left: ${spacing[4]}; + `, + thumbnail: css` + width: 48px; + height: 48px; + border-radius: ${borderRadius[4]}; + `, + errorMessage: css` + height: 100px; + display: flex; + align-items: center; + justify-content: center; + `, +}; diff --git a/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx new file mode 100644 index 0000000000..4a86fd398a --- /dev/null +++ b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx @@ -0,0 +1,185 @@ +import Checkbox from '@Atoms/CheckBox'; +import { LoadingSection } from '@Atoms/LoadingSpinner'; +import { borderRadius, colorTokens, spacing } from '@Config/styles'; +import { typography } from '@Config/typography'; +import { usePaginatedTable } from '@Hooks/usePaginatedTable'; +import Paginator from '@Molecules/Paginator'; +import Table, { type Column } from '@Molecules/Table'; +import { css } from '@emotion/react'; + +import coursePlaceholder from '@Images/course-placeholder.png'; +import { __, sprintf } from '@wordpress/i18n'; +import type { UseFormReturn } from 'react-hook-form'; +import SearchField from './SearchField'; +import { type Course, useCourseCategoryQuery } from '@/v3/shared/services/course_category'; + +interface CourseListTableProps { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + form: UseFormReturn; + type: 'bundles' | 'courses'; +} + +const CourseListTable = ({ type, form }: CourseListTableProps) => { + const courseList: Course[] = form.watch(type) || []; + const { pageInfo, onPageChange, itemsPerPage, offset, onFilterItems } = usePaginatedTable({ + updateQueryParams: false, + }); + const courseListQuery = useCourseCategoryQuery({ + type: type === 'courses' ? 'specific_courses' : 'specific_bundles', + offset, + limit: itemsPerPage, + filter: pageInfo.filter, + }); + + function toggleSelection(isChecked = false) { + form.setValue(type, isChecked ? (courseListQuery.data?.results as Course[]) : []); + } + + function handleAllIsChecked() { + return ( + courseList.length === courseListQuery.data?.results.length && + courseList?.every((item) => courseListQuery.data?.results?.map((result) => result.id).includes(item.id)) + ); + } + + const columns: Column[] = [ + { + Header: courseListQuery.data?.results.length ? ( + + ) : ( + '#' + ), + Cell: (item) => { + return ( +
+ { + const filteredItems = courseList.filter((course) => course.id !== item.id); + const isNewItem = filteredItems?.length === courseList.length; + + if (isNewItem) { + form.setValue(type, [...filteredItems, item]); + } else { + form.setValue(type, filteredItems); + } + }} + checked={courseList.map((course) => course.id).includes(item.id)} + /> + {__('course +
+
{item.title}
+

{item.author}

+
+
+ ); + }, + }, + { + Header: __('Price', 'tutor'), + Cell: (item) => { + return ( +
+ {item.plan_start_price ? ( + {sprintf(__('Starting from %s', 'tutor'), item.plan_start_price)} + ) : ( + <> + {item.sale_price ? item.sale_price : item.regular_price} + {item.sale_price && {item.regular_price}} + + )} +
+ ); + }, + }, + ]; + + if (courseListQuery.isLoading) { + return ; + } + + if (!courseListQuery.data) { + return
{__('Something went wrong', 'tutor')}
; + } + + return ( + <> +
+ +
+ +
+
+ + +
+ +
+ + ); +}; + +export default CourseListTable; + +const styles = { + tableActions: css` + padding: ${spacing[20]}; + `, + tableWrapper: css` + max-height: calc(100vh - 350px); + overflow: auto; + `, + paginatorWrapper: css` + margin: ${spacing[20]} ${spacing[16]}; + `, + checkboxWrapper: css` + display: flex; + align-items: center; + gap: ${spacing[12]}; + `, + courseItem: css` + ${typography.caption()}; + margin-left: ${spacing[4]}; + `, + thumbnail: css` + width: 48px; + height: 48px; + border-radius: ${borderRadius[4]}; + `, + checkboxLabel: css` + ${typography.body()}; + color: ${colorTokens.text.primary}; + `, + price: css` + display: flex; + gap: ${spacing[4]}; + justify-content: end; + `, + discountPrice: css` + text-decoration: line-through; + color: ${colorTokens.text.subdued}; + `, + errorMessage: css` + height: 100px; + display: flex; + align-items: center; + justify-content: center; + `, + startingFrom: css` + color: ${colorTokens.text.hints}; + `, +}; diff --git a/assets/react/v3/shared/components/modals/CourseCategorySelectModal/SearchField.tsx b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/SearchField.tsx new file mode 100644 index 0000000000..52251faa35 --- /dev/null +++ b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/SearchField.tsx @@ -0,0 +1,44 @@ +import SVGIcon from '@Atoms/SVGIcon'; +import FormInputWithContent from '@Components/fields/FormInputWithContent'; +import { useDebounce } from '@Hooks/useDebounce'; +import { useFormWithGlobalError } from '@Hooks/useFormWithGlobalError'; +import type { Filter } from '@Hooks/usePaginatedTable'; +import { __ } from '@wordpress/i18n'; +import { useEffect } from 'react'; +import { Controller } from 'react-hook-form'; + +interface FilterFormValues { + search: string; +} + +interface SearchFieldProps { + onFilterItems: (filter: Filter) => void; +} + +const SearchField = ({ onFilterItems }: SearchFieldProps) => { + const actionsForm = useFormWithGlobalError({ defaultValues: { search: '' } }); + const searchValue = useDebounce(actionsForm.watch('search')); + + useEffect(() => { + onFilterItems({ + ...(searchValue.length > 0 && { search: searchValue }), + }); + }, [onFilterItems, searchValue]); + + return ( + ( + } + placeholder={__('Search...', 'tutor')} + showVerticalBar={false} + /> + )} + /> + ); +}; + +export default SearchField; diff --git a/assets/react/v3/shared/components/modals/CourseCategorySelectModal/index.tsx b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/index.tsx new file mode 100644 index 0000000000..826936c40b --- /dev/null +++ b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/index.tsx @@ -0,0 +1,62 @@ +import Button from '@Atoms/Button'; +import BasicModalWrapper from '@Components/modals/BasicModalWrapper'; +import type { ModalProps } from '@Components/modals/Modal'; +import { spacing } from '@Config/styles'; +import Show from '@Controls/Show'; +import { useFormWithGlobalError } from '@Hooks/useFormWithGlobalError'; +import { css } from '@emotion/react'; +import { __ } from '@wordpress/i18n'; +import type { UseFormReturn } from 'react-hook-form'; +import CategoryListTable from './CategoryListTable'; +import CourseListTable from './CourseListTable'; + +interface CourseCategorySelectModalProps extends ModalProps { + closeModal: (props?: { action: 'CONFIRM' | 'CLOSE' }) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + form: UseFormReturn; + type: 'bundles' | 'courses' | 'categories'; +} + +function CourseCategorySelectModal({ title, closeModal, actions, form, type }: CourseCategorySelectModalProps) { + const _form = useFormWithGlobalError({ + defaultValues: form.getValues(), + }); + + function handleApply() { + form.setValue(type, _form.getValues(type)); + closeModal({ action: 'CONFIRM' }); + } + + return ( + closeModal({ action: 'CLOSE' })} title={title} actions={actions} maxWidth={720}> + } + > + + +
+ + +
+
+ ); +} + +export default CourseCategorySelectModal; + +const styles = { + footer: css` + box-shadow: 0px 1px 0px 0px #e4e5e7 inset; + height: 56px; + display: flex; + align-items: center; + justify-content: end; + gap: ${spacing[16]}; + padding-inline: ${spacing[16]}; + `, +}; diff --git a/assets/react/v3/shared/config/icon-list.ts b/assets/react/v3/shared/config/icon-list.ts index d992d3ff7b..12bd2ab299 100644 --- a/assets/react/v3/shared/config/icon-list.ts +++ b/assets/react/v3/shared/config/icon-list.ts @@ -68,7 +68,7 @@ const collection = { viewBox: '0 0 14 12', }, storeImage: { - icon: '', + icon: '', viewBox: '0 0 28 22', }, storeEye: { @@ -84,7 +84,7 @@ const collection = { viewBox: '0 0 20 20', }, coupon: { - icon: '', + icon: '', viewBox: '0 0 20 20', }, seo: { @@ -116,7 +116,7 @@ const collection = { viewBox: '0 0 20 20', }, preview: { - icon: '', + icon: '', viewBox: '0 0 20 20', }, threeDots: { @@ -128,7 +128,7 @@ const collection = { viewBox: '0 0 32 32', }, plusSquare: { - icon: '', + icon: '', viewBox: '0 0 16 16', }, plusSquareBrand: { @@ -136,7 +136,7 @@ const collection = { viewBox: '0 0 25 24', }, minusSquare: { - icon: '', + icon: '', viewBox: '0 0 16 16', }, markCircle: { @@ -228,7 +228,7 @@ const collection = { viewBox: '0 0 26 26', }, calendar: { - icon: '', + icon: '', viewBox: '0 0 18 18', }, pauseCircle: { diff --git a/assets/react/v3/shared/services/course_category.ts b/assets/react/v3/shared/services/course_category.ts new file mode 100644 index 0000000000..cfc97af478 --- /dev/null +++ b/assets/react/v3/shared/services/course_category.ts @@ -0,0 +1,45 @@ +import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import { wpAjaxInstance } from '../utils/api'; +import { type PaginatedParams, type PaginatedResult } from '../utils/types'; +import endpoints from '../utils/endpoints'; + +export interface Category { + id: number; + title: string; + image: string; + total_courses: number; +} + +export interface Course { + id: number; + title: string; + image: ''; + author: string; + regular_price: string; + sale_price: string | null; + plan_start_price?: string; +} + +interface GetCourseCategoryParam extends PaginatedParams { + type: 'specific_courses' | 'specific_bundles' | 'specific_category'; +} + +const getCourseCategoryList = (params: GetCourseCategoryParam) => { + return wpAjaxInstance.get>(endpoints.COUPON_APPLIES_TO, { + params: { + ...params, + }, + }); +}; + +export const useCourseCategoryQuery = (params: GetCourseCategoryParam) => { + return useQuery({ + queryKey: ['CourseCategory', params], + placeholderData: keepPreviousData, + queryFn: () => { + return getCourseCategoryList(params).then((res) => { + return res.data; + }); + }, + }); +}; From 8025c0cadb17682aadd2e19e50056e4196e4c00f Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Tue, 31 Dec 2024 17:13:45 +0600 Subject: [PATCH 12/18] Todo comment added --- .../pro/membership-settings/components/MembershipItem.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx index 198334d381..0aeb652656 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx @@ -73,6 +73,7 @@ export default function MembershipItem({ data, index }: MembershipItemProps) {
+ {/* @TODO: The icon will change */}
From a07c5b9079ca38d4875fe69774e1c6532e77b655 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Wed, 1 Jan 2025 10:11:53 +0600 Subject: [PATCH 13/18] Update webpack.config.js --- webpack.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 9df043a527..dec2ae6715 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -51,8 +51,8 @@ module.exports = (env, options) => { config.optimization = { splitChunks: { cacheGroups: { - shared: { - test: /[\\/]assets[\\/]react[\\/]v3[\\/]shared[\\/]/, + commons: { + test: /[\\/]node_modules[\\/]/, name: 'tutor-shared.min', chunks: 'all', }, From 7d44fbbed18b53739a8c767850475b80bffc62f4 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Wed, 1 Jan 2025 10:12:10 +0600 Subject: [PATCH 14/18] Lazy added for settings apps --- .../react/v3/entries/payment-settings/components/App.tsx | 9 ++++++--- .../entries/pro/membership-settings/components/App.tsx | 9 ++++++--- assets/react/v3/entries/tax-settings/components/App.tsx | 9 ++++++--- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/assets/react/v3/entries/payment-settings/components/App.tsx b/assets/react/v3/entries/payment-settings/components/App.tsx index 643840103b..273e3dc2ef 100644 --- a/assets/react/v3/entries/payment-settings/components/App.tsx +++ b/assets/react/v3/entries/payment-settings/components/App.tsx @@ -1,6 +1,6 @@ import { Global } from '@emotion/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { useState } from 'react'; +import { lazy, Suspense, useState } from 'react'; import ToastProvider from '@Atoms/Toast'; @@ -8,8 +8,9 @@ import RTLProvider from '@Components/RTLProvider'; import { ModalProvider } from '@Components/modals/Modal'; import { createGlobalCss } from '@Utils/style-utils'; -import PaymentSettings from './PaymentSettings'; import { PaymentProvider } from '../contexts/payment-context'; +import { LoadingSection } from '@/v3/shared/atoms/LoadingSpinner'; +const PaymentSettings = lazy(() => import('./PaymentSettings')); function App() { const [queryClient] = useState( @@ -36,7 +37,9 @@ function App() { - + }> + + diff --git a/assets/react/v3/entries/pro/membership-settings/components/App.tsx b/assets/react/v3/entries/pro/membership-settings/components/App.tsx index 693222deae..9e3e5cf5d4 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/App.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/App.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { lazy, Suspense, useState } from 'react'; import { Global } from '@emotion/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryParamProvider } from 'use-query-params'; @@ -10,7 +10,8 @@ import RTLProvider from '@Components/RTLProvider'; import { ModalProvider } from '@Components/modals/Modal'; import { createGlobalCss } from '@Utils/style-utils'; -import MembershipSettings from './MembershipSettings'; +import { LoadingSection } from '@/v3/shared/atoms/LoadingSpinner'; +const MembershipSettings = lazy(() => import('./MembershipSettings')); function App() { const [queryClient] = useState( @@ -37,7 +38,9 @@ function App() { - + }> + + diff --git a/assets/react/v3/entries/tax-settings/components/App.tsx b/assets/react/v3/entries/tax-settings/components/App.tsx index 17e2232af7..fe2a580bc7 100644 --- a/assets/react/v3/entries/tax-settings/components/App.tsx +++ b/assets/react/v3/entries/tax-settings/components/App.tsx @@ -1,6 +1,6 @@ import { Global } from '@emotion/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { useState } from 'react'; +import { lazy, Suspense, useState } from 'react'; import ToastProvider from '@Atoms/Toast'; @@ -8,7 +8,8 @@ import RTLProvider from '@Components/RTLProvider'; import { ModalProvider } from '@Components/modals/Modal'; import { createGlobalCss } from '@Utils/style-utils'; -import TaxSettingsPage from './TaxSettings'; +import { LoadingSection } from '@/v3/shared/atoms/LoadingSpinner'; +const TaxSettingsPage = lazy(() => import('./TaxSettings')); function App() { const [queryClient] = useState( @@ -34,7 +35,9 @@ function App() { - + }> + + From 1cb6b03648646309aeaff072079b26f4817197e9 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Wed, 1 Jan 2025 11:14:46 +0600 Subject: [PATCH 15/18] applies_to type changed --- .../modals/CourseCategorySelectModal/CategoryListTable.tsx | 2 +- .../modals/CourseCategorySelectModal/CourseListTable.tsx | 2 +- assets/react/v3/shared/services/course_category.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx index b80fc0cadc..7b0f26eb6c 100644 --- a/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx +++ b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CategoryListTable.tsx @@ -24,7 +24,7 @@ const CategoryListTable = ({ form }: CategoryListTableProps) => { updateQueryParams: false, }); const categoryListQuery = useCourseCategoryQuery({ - type: 'specific_category', + applies_to: 'specific_category', offset, limit: itemsPerPage, filter: pageInfo.filter, diff --git a/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx index 4a86fd398a..55937a2981 100644 --- a/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx +++ b/assets/react/v3/shared/components/modals/CourseCategorySelectModal/CourseListTable.tsx @@ -25,7 +25,7 @@ const CourseListTable = ({ type, form }: CourseListTableProps) => { updateQueryParams: false, }); const courseListQuery = useCourseCategoryQuery({ - type: type === 'courses' ? 'specific_courses' : 'specific_bundles', + applies_to: type === 'courses' ? 'specific_courses' : 'specific_bundles', offset, limit: itemsPerPage, filter: pageInfo.filter, diff --git a/assets/react/v3/shared/services/course_category.ts b/assets/react/v3/shared/services/course_category.ts index cfc97af478..41939a50f6 100644 --- a/assets/react/v3/shared/services/course_category.ts +++ b/assets/react/v3/shared/services/course_category.ts @@ -21,7 +21,7 @@ export interface Course { } interface GetCourseCategoryParam extends PaginatedParams { - type: 'specific_courses' | 'specific_bundles' | 'specific_category'; + applies_to: 'specific_courses' | 'specific_bundles' | 'specific_category'; } const getCourseCategoryList = (params: GetCourseCategoryParam) => { From f40618f7292b54e1f3aa23973c5b696fe1025ceb Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Wed, 1 Jan 2025 11:15:20 +0600 Subject: [PATCH 16/18] Membership form fields component added --- .../components/Categories.tsx | 4 +- .../components/MembershipFormFields.tsx | 444 +++++++++++++++++ .../components/modals/MembershipModal.tsx | 454 +----------------- 3 files changed, 455 insertions(+), 447 deletions(-) create mode 100644 assets/react/v3/entries/pro/membership-settings/components/MembershipFormFields.tsx diff --git a/assets/react/v3/entries/pro/membership-settings/components/Categories.tsx b/assets/react/v3/entries/pro/membership-settings/components/Categories.tsx index d29568c433..74d07be7a0 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/Categories.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/Categories.tsx @@ -1,6 +1,5 @@ import Show from '@/v3/shared/controls/Show'; import { type MembershipFormData } from '../services/memberships'; -import { type FormWithGlobalErrorType } from '@/v3/shared/hooks/useFormWithGlobalError'; import For from '@/v3/shared/controls/For'; import CategoryItem from './CategoryItem'; import { __, sprintf } from '@wordpress/i18n'; @@ -10,9 +9,10 @@ import Button from '@/v3/shared/atoms/Button'; import CourseCategorySelectModal from '@/v3/shared/components/modals/CourseCategorySelectModal'; import { css } from '@emotion/react'; import { borderRadius, colorTokens } from '@/v3/shared/config/styles'; +import { type UseFormReturn } from 'react-hook-form'; interface CategoriesProps { - form: FormWithGlobalErrorType; + form: UseFormReturn; } export default function Categories({ form }: CategoriesProps) { diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipFormFields.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipFormFields.tsx new file mode 100644 index 0000000000..78c7af448f --- /dev/null +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipFormFields.tsx @@ -0,0 +1,444 @@ +import { css } from '@emotion/react'; +import { __, sprintf } from '@wordpress/i18n'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { borderRadius, Breakpoint, colorTokens, spacing } from '@Config/styles'; +import { typography } from '@Config/typography'; +import FormInput from '@/v3/shared/components/fields/FormInput'; +import FormInputWithContent from '@/v3/shared/components/fields/FormInputWithContent'; +import { styleUtils } from '@/v3/shared/utils/style-utils'; +import FormSelectInput from '@/v3/shared/components/fields/FormSelectInput'; +import { requiredRule } from '@Utils/validation'; +import { tutorConfig } from '@/v3/shared/config/config'; +import FormInputWithPresets from '@/v3/shared/components/fields/FormInputWithPresets'; +import FormCheckbox from '@/v3/shared/components/fields/FormCheckbox'; +import Show from '@/v3/shared/controls/Show'; +import FormTimeInput from '@/v3/shared/components/fields/FormTimeInput'; +import FormDateInput from '@/v3/shared/components/fields/FormDateInput'; +import FormSwitch from '@/v3/shared/components/fields/FormSwitch'; +import { CURRENT_VIEWPORT } from '@/v3/shared/config/constants'; + +import FormRadioGroup from '@/v3/shared/components/fields/FormRadioGroup'; +import IconsAndFeatures from './IconsAndFeatures'; +import Categories from './Categories'; +import { type MembershipFormData } from '../services/memberships'; +const { tutor_currency } = tutorConfig; + +export default function MembershipFormFields() { + const form = useFormContext(); + + const chargeEnrolmentFee = form.watch('charge_enrollment_fee'); + const hasSale = form.watch('offer_sale_price'); + const regularPrice = form.watch('regular_price'); + const hasScheduledSale = !!form.watch('schedule_sale_price'); + const isFeatured = !!form.watch('is_featured'); + + const lifetimePresets = [3, 6, 9, 12]; + const lifetimeOptions = [ + ...lifetimePresets.map((preset) => ({ + label: sprintf(__('%s times', 'tutor'), preset.toString()), + value: String(preset), + })), + { + label: __('Until cancelled', 'tutor'), + value: 'Until cancelled', + }, + ]; + + const planType = form.watch('plan_type'); + const planTypeOptions = [ + { label: __('Full Site', 'tutor'), value: 'full_site' }, + { label: __('Specific Categories', 'tutor'), value: 'category' }, + ]; + + return ( +
+ ( + + )} + /> + + ( + + )} + /> + +
+ { + if (Number(value) <= 0) { + return __('Price must be greater than 0', 'tutor'); + } + }, + }} + render={(controllerProps) => ( + + )} + /> + + { + if (Number(value) < 1) { + return __('This value must be equal to or greater than 1', 'tutor'); + } + }, + }} + render={(controllerProps) => ( + + )} + /> + + ( +  
: __('Recurring Options', 'tutor')} + options={[ + { label: __('Day(s)', 'tutor'), value: 'day' }, + { label: __('Week(s)', 'tutor'), value: 'week' }, + { label: __('Month(s)', 'tutor'), value: 'month' }, + { label: __('Year(s)', 'tutor'), value: 'year' }, + ]} + removeOptionsMinWidth + /> + )} + /> + + { + if (value === 'Until cancelled') { + return true; + } + + if (Number(value) <= 0) { + return __('Renew plan must be greater than 0', 'tutor'); + } + return true; + }, + }} + render={(controllerProps) => ( + + )} + /> +
+ + ( + + )} + /> + + + + + + + + } + /> + + + { + if (Number(value) <= 0) { + return __('Enrollment fee must be greater than 0', 'tutor'); + } + return true; + }, + }} + render={(controllerProps) => ( + + )} + /> + + + ( + + )} + /> + + } + /> + + + } + /> + + +
+
+ } + /> +
+ +
+ { + if (value && regularPrice && Number(value) >= Number(regularPrice)) { + return __('Sale price should be less than regular price', 'tutor'); + } + + if (value && regularPrice && Number(value) <= 0) { + return __('Sale price should be greater than 0', 'tutor'); + } + + return undefined; + }, + }} + render={(props) => ( + + )} + /> + + } + /> + + +
+ +
+ ( + + )} + /> + + ( + + )} + /> +
+
+
+ +
+ { + const startDate = form.watch(`sale_price_from_date`); + const endDate = value; + if (startDate && endDate) { + return new Date(startDate) > new Date(endDate) + ? __('Sales End date should be greater than start date', 'tutor') + : undefined; + } + return undefined; + }, + }, + deps: ['sale_price_from_date'], + }} + render={(controllerProps) => ( + + )} + /> + + { + const startDate = form.watch(`sale_price_from_date`); + const startTime = form.watch(`sale_price_from_time`); + const endDate = form.watch(`sale_price_to_date`); + const endTime = value; + if (startDate && endDate && startTime && endTime) { + return new Date(`${startDate} ${startTime}`) > new Date(`${endDate} ${endTime}`) + ? __('Sales End time should be greater than start time', 'tutor') + : undefined; + } + return undefined; + }, + }, + deps: ['sale_price_from_date', 'sale_price_from_time', 'sale_price_to_date'], + }} + render={(controllerProps) => ( + + )} + /> +
+
+
+
+
+
+
+ ); +} + +const styles = { + container: css` + width: 100%; + max-width: 640px; + margin: 0 auto; + border: 1px solid ${colorTokens.stroke.default}; + border-radius: ${borderRadius.card}; + padding: ${spacing[16]}; + + display: flex; + flex-direction: column; + gap: ${spacing[12]}; + `, + salePriceWrapper: css` + background-color: ${colorTokens.background.white}; + display: flex; + flex-direction: column; + gap: ${spacing[20]}; + + padding: ${spacing[12]}; + border: 1px solid ${colorTokens.stroke.default}; + border-radius: ${borderRadius.card}; + `, + salePriceInputs: css` + display: flex; + flex-direction: column; + gap: ${spacing[8]}; + `, + inputGroup: css` + display: grid; + grid-template-columns: 1fr 0.7fr 1fr 1fr; + align-items: start; + gap: ${spacing[8]}; + + ${Breakpoint.mobile} { + grid-template-columns: 1fr; + } + `, + datetimeWrapper: css` + label { + ${typography.caption()}; + color: ${colorTokens.text.title}; + } + `, + planTypeWrapper: css` + display: flex; + gap: ${spacing[8]}; + `, +}; diff --git a/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx b/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx index ca91326632..6dd3f71c38 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/modals/MembershipModal.tsx @@ -1,6 +1,7 @@ +import { lazy, Suspense, useEffect } from 'react'; import { css } from '@emotion/react'; -import { __, sprintf } from '@wordpress/i18n'; -import { Controller, FormProvider } from 'react-hook-form'; +import { __ } from '@wordpress/i18n'; +import { FormProvider } from 'react-hook-form'; import Button from '@Atoms/Button'; import SVGIcon from '@Atoms/SVGIcon'; @@ -8,23 +9,8 @@ import SVGIcon from '@Atoms/SVGIcon'; import { type ModalProps } from '@Components/modals/Modal'; import ModalWrapper from '@Components/modals/ModalWrapper'; -import { borderRadius, Breakpoint, colorTokens, spacing } from '@Config/styles'; -import { typography } from '@Config/typography'; +import { Breakpoint, spacing } from '@Config/styles'; import { useFormWithGlobalError } from '@Hooks/useFormWithGlobalError'; -import FormInput from '@/v3/shared/components/fields/FormInput'; -import FormInputWithContent from '@/v3/shared/components/fields/FormInputWithContent'; -import { styleUtils } from '@/v3/shared/utils/style-utils'; -import FormSelectInput from '@/v3/shared/components/fields/FormSelectInput'; -import { requiredRule } from '@Utils/validation'; -import { tutorConfig } from '@/v3/shared/config/config'; -import FormInputWithPresets from '@/v3/shared/components/fields/FormInputWithPresets'; -import FormCheckbox from '@/v3/shared/components/fields/FormCheckbox'; -import Show from '@/v3/shared/controls/Show'; -import FormTimeInput from '@/v3/shared/components/fields/FormTimeInput'; -import FormDateInput from '@/v3/shared/components/fields/FormDateInput'; -import FormSwitch from '@/v3/shared/components/fields/FormSwitch'; -import IconsAndFeatures from '../IconsAndFeatures'; -import { CURRENT_VIEWPORT } from '@/v3/shared/config/constants'; import { convertFormDataToPayload, convertPlanToFormData, @@ -32,10 +18,8 @@ import { type MembershipPlan, useSaveMembershipPlanMutation, } from '../../services/memberships'; -import { useEffect } from 'react'; -import FormRadioGroup from '@/v3/shared/components/fields/FormRadioGroup'; -import Categories from '../Categories'; -const { tutor_currency } = tutorConfig; +import { LoadingSection } from '@/v3/shared/atoms/LoadingSpinner'; +const MembershipFormFields = lazy(() => import('../MembershipFormFields')); interface MembershipModalProps extends ModalProps { closeModal: (props?: { action: 'CONFIRM' | 'CLOSE' }) => void; @@ -68,30 +52,6 @@ export default function MembershipModal({ title, subtitle, icon, plan, closeModa const isFormDirty = form.formState.isDirty; - const chargeEnrolmentFee = form.watch('charge_enrollment_fee'); - const hasSale = form.watch('offer_sale_price'); - const regularPrice = form.watch('regular_price'); - const hasScheduledSale = !!form.watch('schedule_sale_price'); - const isFeatured = !!form.watch('is_featured'); - - const lifetimePresets = [3, 6, 9, 12]; - const lifetimeOptions = [ - ...lifetimePresets.map((preset) => ({ - label: sprintf(__('%s times', 'tutor'), preset.toString()), - value: String(preset), - })), - { - label: __('Until cancelled', 'tutor'), - value: 'Until cancelled', - }, - ]; - - const planType = form.watch('plan_type'); - const planTypeOptions = [ - { label: __('Full Site', 'tutor'), value: 'full_site' }, - { label: __('Specific Categories', 'tutor'), value: 'category' }, - ]; - return (
-
- ( - - )} - /> - - ( - - )} - /> - -
- { - if (Number(value) <= 0) { - return __('Price must be greater than 0', 'tutor'); - } - }, - }} - render={(controllerProps) => ( - - )} - /> - - { - if (Number(value) < 1) { - return __('This value must be equal to or greater than 1', 'tutor'); - } - }, - }} - render={(controllerProps) => ( - - )} - /> - - ( -  
: __('Recurring Options', 'tutor')} - options={[ - { label: __('Day(s)', 'tutor'), value: 'day' }, - { label: __('Week(s)', 'tutor'), value: 'week' }, - { label: __('Month(s)', 'tutor'), value: 'month' }, - { label: __('Year(s)', 'tutor'), value: 'year' }, - ]} - removeOptionsMinWidth - /> - )} - /> - - { - if (value === 'Until cancelled') { - return true; - } - - if (Number(value) <= 0) { - return __('Renew plan must be greater than 0', 'tutor'); - } - return true; - }, - }} - render={(controllerProps) => ( - - )} - /> -
- - ( - - )} - /> - - - - - - - - ( - - )} - /> - - - { - if (Number(value) <= 0) { - return __('Enrollment fee must be greater than 0', 'tutor'); - } - return true; - }, - }} - render={(controllerProps) => ( - - )} - /> - - - ( - - )} - /> - - ( - - )} - /> - - - } - /> - - -
-
- } - /> -
- -
- { - if (value && regularPrice && Number(value) >= Number(regularPrice)) { - return __('Sale price should be less than regular price', 'tutor'); - } - - if (value && regularPrice && Number(value) <= 0) { - return __('Sale price should be greater than 0', 'tutor'); - } - - return undefined; - }, - }} - render={(props) => ( - - )} - /> - - } - /> - - -
- -
- ( - - )} - /> - - ( - - )} - /> -
-
-
- -
- { - const startDate = form.watch(`sale_price_from_date`); - const endDate = value; - if (startDate && endDate) { - return new Date(startDate) > new Date(endDate) - ? __('Sales End date should be greater than start date', 'tutor') - : undefined; - } - return undefined; - }, - }, - deps: ['sale_price_from_date'], - }} - render={(controllerProps) => ( - - )} - /> - - { - const startDate = form.watch(`sale_price_from_date`); - const startTime = form.watch(`sale_price_from_time`); - const endDate = form.watch(`sale_price_to_date`); - const endTime = value; - if (startDate && endDate && startTime && endTime) { - return new Date(`${startDate} ${startTime}`) > new Date(`${endDate} ${endTime}`) - ? __('Sales End time should be greater than start time', 'tutor') - : undefined; - } - return undefined; - }, - }, - deps: ['sale_price_from_date', 'sale_price_from_time', 'sale_price_to_date'], - }} - render={(controllerProps) => ( - - )} - /> -
-
-
-
-
-
-
+ }> + +
@@ -489,51 +100,4 @@ const styles = { padding: ${spacing[24]} ${spacing[16]}; } `, - container: css` - width: 100%; - max-width: 640px; - margin: 0 auto; - border: 1px solid ${colorTokens.stroke.default}; - border-radius: ${borderRadius.card}; - padding: ${spacing[16]}; - - display: flex; - flex-direction: column; - gap: ${spacing[12]}; - `, - salePriceWrapper: css` - background-color: ${colorTokens.background.white}; - display: flex; - flex-direction: column; - gap: ${spacing[20]}; - - padding: ${spacing[12]}; - border: 1px solid ${colorTokens.stroke.default}; - border-radius: ${borderRadius.card}; - `, - salePriceInputs: css` - display: flex; - flex-direction: column; - gap: ${spacing[8]}; - `, - inputGroup: css` - display: grid; - grid-template-columns: 1fr 0.7fr 1fr 1fr; - align-items: start; - gap: ${spacing[8]}; - - ${Breakpoint.mobile} { - grid-template-columns: 1fr; - } - `, - datetimeWrapper: css` - label { - ${typography.caption()}; - color: ${colorTokens.text.title}; - } - `, - planTypeWrapper: css` - display: flex; - gap: ${spacing[8]}; - `, }; From a2839da045b9abd18b211c124f39e2eaf6d6d603 Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Wed, 1 Jan 2025 12:11:10 +0600 Subject: [PATCH 17/18] priceTag added to membership item --- .../pro/membership-settings/components/MembershipItem.tsx | 7 +++++-- assets/react/v3/shared/config/icon-list.ts | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx index 0aeb652656..9be1fcede4 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx @@ -73,8 +73,7 @@ export default function MembershipItem({ data, index }: MembershipItemProps) {
- {/* @TODO: The icon will change */} - +
{data.plan_name} @@ -213,6 +212,10 @@ const styles = { display: flex; align-items: center; gap: ${spacing[12]}; + + svg { + color: ${colorTokens.icon.default}; + } `, planInfo: css` display: flex; diff --git a/assets/react/v3/shared/config/icon-list.ts b/assets/react/v3/shared/config/icon-list.ts index 12bd2ab299..a0d88b853c 100644 --- a/assets/react/v3/shared/config/icon-list.ts +++ b/assets/react/v3/shared/config/icon-list.ts @@ -871,6 +871,10 @@ const collection = { icon: '', viewBox: '0 0 28 28', }, + priceTag: { + icon: '', + viewBox: '0 0 32 32', + }, } as const; export default collection; From 0fabfa71fb9be785ff05d0b64fb5801b1efa6cde Mon Sep 17 00:00:00 2001 From: Sazedul Haque Date: Wed, 1 Jan 2025 12:32:14 +0600 Subject: [PATCH 18/18] Update MembershipItem.tsx --- .../pro/membership-settings/components/MembershipItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx index 9be1fcede4..be4a97e603 100644 --- a/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx +++ b/assets/react/v3/entries/pro/membership-settings/components/MembershipItem.tsx @@ -117,7 +117,7 @@ export default function MembershipItem({ data, index }: MembershipItemProps) {
} />