-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1494 from themeum/membership-frontend
Membership Frontend: Modals, Features, and API Integrations
- Loading branch information
Showing
29 changed files
with
2,515 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
assets/react/v3/entries/pro/membership-settings/components/App.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { lazy, Suspense, useState } from 'react'; | ||
import { Global } from '@emotion/react'; | ||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | ||
import { QueryParamProvider } from 'use-query-params'; | ||
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'; | ||
|
||
import ToastProvider from '@Atoms/Toast'; | ||
|
||
import RTLProvider from '@Components/RTLProvider'; | ||
import { ModalProvider } from '@Components/modals/Modal'; | ||
|
||
import { createGlobalCss } from '@Utils/style-utils'; | ||
import { LoadingSection } from '@/v3/shared/atoms/LoadingSpinner'; | ||
const MembershipSettings = lazy(() => import('./MembershipSettings')); | ||
|
||
function App() { | ||
const [queryClient] = useState( | ||
() => | ||
new QueryClient({ | ||
defaultOptions: { | ||
queries: { | ||
retry: false, | ||
refetchOnWindowFocus: false, | ||
networkMode: 'always', | ||
}, | ||
mutations: { | ||
retry: false, | ||
networkMode: 'always', | ||
}, | ||
}, | ||
}), | ||
); | ||
|
||
return ( | ||
<RTLProvider> | ||
<QueryParamProvider adapter={ReactRouter6Adapter}> | ||
<QueryClientProvider client={queryClient}> | ||
<ToastProvider position="bottom-right"> | ||
<ModalProvider> | ||
<Global styles={createGlobalCss()} /> | ||
<Suspense fallback={<LoadingSection />}> | ||
<MembershipSettings /> | ||
</Suspense> | ||
</ModalProvider> | ||
</ToastProvider> | ||
</QueryClientProvider> | ||
</QueryParamProvider> | ||
</RTLProvider> | ||
); | ||
} | ||
|
||
export default App; |
85 changes: 85 additions & 0 deletions
85
assets/react/v3/entries/pro/membership-settings/components/Categories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import Show from '@/v3/shared/controls/Show'; | ||
import { type MembershipFormData } from '../services/memberships'; | ||
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'; | ||
import { type UseFormReturn } from 'react-hook-form'; | ||
|
||
interface CategoriesProps { | ||
form: UseFormReturn<MembershipFormData>; | ||
} | ||
|
||
export default function Categories({ form }: CategoriesProps) { | ||
const { showModal } = useModal(); | ||
const categories = form.watch('categories'); | ||
|
||
return ( | ||
<> | ||
<Show when={categories.length}> | ||
<div css={styles.categoriesWrapper}> | ||
<For each={categories}> | ||
{(category) => ( | ||
<CategoryItem | ||
title={category.title} | ||
subTitle={sprintf(__('%s Courses', 'tutor'), category.total_courses)} | ||
image={category.image} | ||
handleDeleteClick={() => { | ||
form.setValue( | ||
'categories', | ||
categories.filter((item) => item.id !== category.id), | ||
{ shouldDirty: true }, | ||
); | ||
}} | ||
/> | ||
)} | ||
</For> | ||
</div> | ||
</Show> | ||
|
||
<Button | ||
variant="tertiary" | ||
isOutlined | ||
buttonCss={styles.addCategoriesButton} | ||
icon={<SVGIcon name="plusSquareBrand" width={24} height={25} />} | ||
onClick={() => { | ||
showModal({ | ||
component: CourseCategorySelectModal, | ||
props: { | ||
title: __('Selected items', 'tutor'), | ||
type: 'categories', | ||
form, | ||
}, | ||
closeOnOutsideClick: true, | ||
depthIndex: 9999, | ||
}); | ||
}} | ||
> | ||
{__('Add Categories', 'tutor')} | ||
</Button> | ||
</> | ||
); | ||
} | ||
|
||
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; | ||
} | ||
`, | ||
}; |
67 changes: 67 additions & 0 deletions
67
assets/react/v3/entries/pro/membership-settings/components/CategoryItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div css={styles.selectedItem}> | ||
<div css={styles.selectedThumb}> | ||
<img src={image || coursePlaceholder} css={styles.thumbnail} alt="course item" /> | ||
</div> | ||
<div css={styles.selectedContent}> | ||
<div css={styles.selectedTitle}>{title}</div> | ||
<div css={styles.selectedSubTitle}>{subTitle}</div> | ||
</div> | ||
<div> | ||
<Button variant="text" onClick={handleDeleteClick}> | ||
<SVGIcon name="delete" width={24} height={24} /> | ||
</Button> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
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; | ||
`, | ||
}; |
111 changes: 111 additions & 0 deletions
111
assets/react/v3/entries/pro/membership-settings/components/IconsAndFeatures.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
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 { 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 ( | ||
<div css={styles.wrapper}> | ||
<div css={styles.header}> | ||
<label>{__('Features', 'tutor')}</label> | ||
<Button variant="text" onClick={() => append({ id: nanoid(), icon: 'tick_circle_fill', content: '' })}> | ||
<SVGIcon name="plus" width={24} height={24} /> | ||
</Button> | ||
</div> | ||
<Show when={fields.length > 0}> | ||
<div css={styles.features}> | ||
<DndContext | ||
sensors={sensors} | ||
modifiers={[restrictToParentElement]} | ||
onDragEnd={(event) => { | ||
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); | ||
} | ||
}} | ||
> | ||
<SortableContext items={fields} strategy={verticalListSortingStrategy}> | ||
<For each={fields}> | ||
{(item, index) => ( | ||
<Controller | ||
key={item.id} | ||
control={form.control} | ||
name={`features.${index}` as 'features.0'} | ||
rules={{ | ||
validate: (value) => !!value?.content || __('Content is required', 'tutor'), | ||
}} | ||
render={(controllerProps) => ( | ||
<FormFeatureItem id={item.id} {...controllerProps} handleDeleteClick={() => remove(index)} /> | ||
)} | ||
/> | ||
)} | ||
</For> | ||
</SortableContext> | ||
</DndContext> | ||
</div> | ||
</Show> | ||
</div> | ||
); | ||
} | ||
|
||
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]}; | ||
`, | ||
}; |
Oops, something went wrong.