mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-17 23:44:39 -05:00
Admin X Tiers Fields (#17332)
refs. https://github.com/TryGhost/Product/issues/3580 - The currency dropdown needed a small version of the regular select - The input field needed a version with a right-side placeholder text
This commit is contained in:
parent
42d87d1437
commit
497d1be2ea
5 changed files with 80 additions and 18 deletions
|
@ -4,7 +4,7 @@ import type {Meta, StoryObj} from '@storybook/react';
|
||||||
import Select, {SelectOption} from './Select';
|
import Select, {SelectOption} from './Select';
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Global / Simple select',
|
title: 'Global / Form / Select',
|
||||||
component: Select,
|
component: Select,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
decorators: [(_story: any) => (<div style={{maxWidth: '400px'}}>{_story()}</div>)],
|
decorators: [(_story: any) => (<div style={{maxWidth: '400px'}}>{_story()}</div>)],
|
||||||
|
@ -61,6 +61,13 @@ export const WithHint: Story = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ExtraSmall: Story = {
|
||||||
|
args: {
|
||||||
|
options: selectOptions,
|
||||||
|
size: 'xs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const WithSelectedOption: Story = {
|
export const WithSelectedOption: Story = {
|
||||||
render: function Component(args) {
|
render: function Component(args) {
|
||||||
const [, updateArgs] = useArgs();
|
const [, updateArgs] = useArgs();
|
||||||
|
|
|
@ -11,6 +11,7 @@ export interface SelectOption {
|
||||||
|
|
||||||
export interface SelectProps {
|
export interface SelectProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
size?: 'xs' | 'md';
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
options: SelectOption[];
|
options: SelectOption[];
|
||||||
selectedOption?: string
|
selectedOption?: string
|
||||||
|
@ -18,6 +19,7 @@ export interface SelectProps {
|
||||||
error?:boolean;
|
error?:boolean;
|
||||||
hint?: React.ReactNode;
|
hint?: React.ReactNode;
|
||||||
clearBg?: boolean;
|
clearBg?: boolean;
|
||||||
|
border?: boolean;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
selectClassName?: string;
|
selectClassName?: string;
|
||||||
optionClassName?: string;
|
optionClassName?: string;
|
||||||
|
@ -26,6 +28,7 @@ export interface SelectProps {
|
||||||
|
|
||||||
const Select: React.FC<SelectProps> = ({
|
const Select: React.FC<SelectProps> = ({
|
||||||
title,
|
title,
|
||||||
|
size = 'md',
|
||||||
prompt,
|
prompt,
|
||||||
options,
|
options,
|
||||||
selectedOption,
|
selectedOption,
|
||||||
|
@ -33,6 +36,7 @@ const Select: React.FC<SelectProps> = ({
|
||||||
error,
|
error,
|
||||||
hint,
|
hint,
|
||||||
clearBg = true,
|
clearBg = true,
|
||||||
|
border = true,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
selectClassName,
|
selectClassName,
|
||||||
optionClassName,
|
optionClassName,
|
||||||
|
@ -49,7 +53,7 @@ const Select: React.FC<SelectProps> = ({
|
||||||
containerClasses = clsx(
|
containerClasses = clsx(
|
||||||
'relative w-full after:pointer-events-none',
|
'relative w-full after:pointer-events-none',
|
||||||
`after:absolute after:block after:h-2 after:w-2 after:rotate-45 after:border-[1px] after:border-l-0 after:border-t-0 after:border-grey-900 after:content-['']`,
|
`after:absolute after:block after:h-2 after:w-2 after:rotate-45 after:border-[1px] after:border-l-0 after:border-t-0 after:border-grey-900 after:content-['']`,
|
||||||
title ? 'after:top-[14px]' : 'after:top-[14px]',
|
size === 'xs' ? 'after:top-[6px]' : 'after:top-[14px]',
|
||||||
clearBg ? 'after:right-0' : 'after:right-4'
|
clearBg ? 'after:right-0' : 'after:right-4'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -61,7 +65,9 @@ const Select: React.FC<SelectProps> = ({
|
||||||
let selectClasses = '';
|
let selectClasses = '';
|
||||||
if (!unstyled) {
|
if (!unstyled) {
|
||||||
selectClasses = clsx(
|
selectClasses = clsx(
|
||||||
'h-10 w-full cursor-pointer appearance-none border-b py-2 pr-5 outline-none',
|
size === 'xs' ? 'h-6 py-0 pr-3 text-xs' : 'h-10 py-2 pr-5',
|
||||||
|
'w-full cursor-pointer appearance-none outline-none',
|
||||||
|
border && 'border-b',
|
||||||
!clearBg && 'bg-grey-75 px-[10px]',
|
!clearBg && 'bg-grey-75 px-[10px]',
|
||||||
error ? 'border-red' : 'border-grey-500 hover:border-grey-700 focus:border-black',
|
error ? 'border-red' : 'border-grey-500 hover:border-grey-700 focus:border-black',
|
||||||
(title && !clearBg) && 'mt-2'
|
(title && !clearBg) && 'mt-2'
|
||||||
|
|
|
@ -58,6 +58,14 @@ export const WithHint: Story = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WithRightPlaceholder: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Monthly price',
|
||||||
|
placeholder: '0',
|
||||||
|
rightPlaceholder: 'USD/month'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const PasswordType: Story = {
|
export const PasswordType: Story = {
|
||||||
args: {
|
args: {
|
||||||
title: 'Password',
|
title: 'Password',
|
||||||
|
|
|
@ -12,6 +12,7 @@ export type TextFieldProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||||
value?: string;
|
value?: string;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
rightPlaceholder?: string;
|
||||||
hint?: React.ReactNode;
|
hint?: React.ReactNode;
|
||||||
clearBg?: boolean;
|
clearBg?: boolean;
|
||||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
@ -33,6 +34,7 @@ const TextField: React.FC<TextFieldProps> = ({
|
||||||
value,
|
value,
|
||||||
error,
|
error,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
rightPlaceholder,
|
||||||
hint,
|
hint,
|
||||||
clearBg = true,
|
clearBg = true,
|
||||||
onChange,
|
onChange,
|
||||||
|
@ -53,10 +55,13 @@ const TextField: React.FC<TextFieldProps> = ({
|
||||||
error ? `border-red` : `${disabled ? 'border-grey-300' : 'border-grey-500 hover:border-grey-700 focus:border-black'}`,
|
error ? `border-red` : `${disabled ? 'border-grey-300' : 'border-grey-500 hover:border-grey-700 focus:border-black'}`,
|
||||||
(title && !hideTitle && !clearBg) && `mt-2`,
|
(title && !hideTitle && !clearBg) && `mt-2`,
|
||||||
(disabled ? 'text-grey-700' : ''),
|
(disabled ? 'text-grey-700' : ''),
|
||||||
|
rightPlaceholder && 'peer w-0 grow',
|
||||||
className
|
className
|
||||||
);
|
);
|
||||||
|
|
||||||
const field = <input
|
let field = <></>;
|
||||||
|
|
||||||
|
const inputField = <input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className={textFieldClasses || className}
|
className={textFieldClasses || className}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -69,6 +74,22 @@ const TextField: React.FC<TextFieldProps> = ({
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
{...props} />;
|
{...props} />;
|
||||||
|
|
||||||
|
if (rightPlaceholder) {
|
||||||
|
const rightPHClasses = !unstyled && clsx(
|
||||||
|
'h-10 border-b py-2 text-right text-grey-500',
|
||||||
|
error ? `border-red` : `${disabled ? 'border-grey-300' : 'border-grey-500 peer-hover:border-grey-700 peer-focus:border-black'}`
|
||||||
|
);
|
||||||
|
|
||||||
|
field = (
|
||||||
|
<div className='flex w-full items-center'>
|
||||||
|
{inputField}
|
||||||
|
<span className={rightPHClasses || ''}>{rightPlaceholder}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
field = inputField;
|
||||||
|
}
|
||||||
|
|
||||||
if (title || hint) {
|
if (title || hint) {
|
||||||
let titleGrey = false;
|
let titleGrey = false;
|
||||||
if (titleColor === 'auto') {
|
if (titleColor === 'auto') {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Icon from '../../../../admin-x-ds/global/Icon';
|
||||||
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Select from '../../../../admin-x-ds/global/form/Select';
|
||||||
import SortableList from '../../../../admin-x-ds/global/SortableList';
|
import SortableList from '../../../../admin-x-ds/global/SortableList';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import TierDetailPreview from './TierDetailPreview';
|
import TierDetailPreview from './TierDetailPreview';
|
||||||
|
@ -88,22 +89,40 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
||||||
onChange={e => updateForm(state => ({...state, description: e.target.value}))}
|
onChange={e => updateForm(state => ({...state, description: e.target.value}))}
|
||||||
/>
|
/>
|
||||||
<div className='flex gap-10'>
|
<div className='flex gap-10'>
|
||||||
<div className='flex basis-1/2 flex-col gap-2'>
|
<div className='basis-1/2'>
|
||||||
|
<div className='mb-1 flex h-6 items-center justify-between'>
|
||||||
|
<Heading level={6}>Prices</Heading>
|
||||||
|
<div className='w-10'>
|
||||||
|
<Select
|
||||||
|
border={false}
|
||||||
|
options={[
|
||||||
|
{label: 'USD', value: 'US Dollaz'},
|
||||||
|
{label: 'HUF', value: 'Hungarian Dollaz'}
|
||||||
|
]}
|
||||||
|
selectClassName='font-medium'
|
||||||
|
size='xs'
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
<TextField
|
<TextField
|
||||||
placeholder='1'
|
placeholder='1'
|
||||||
title='Prices'
|
rightPlaceholder='USD/month'
|
||||||
value={formState.monthly_price}
|
value={formState.monthly_price}
|
||||||
onChange={e => updateForm(state => ({...state, monthly_price: e.target.value.replace(/[^\d.]/, '')}))}
|
onChange={e => updateForm(state => ({...state, monthly_price: e.target.value.replace(/[^\d.]/, '')}))}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
placeholder='10'
|
placeholder='10'
|
||||||
|
rightPlaceholder='USD/year'
|
||||||
value={formState.yearly_price}
|
value={formState.yearly_price}
|
||||||
onChange={e => updateForm(state => ({...state, yearly_price: e.target.value.replace(/[^\d.]/, '')}))}
|
onChange={e => updateForm(state => ({...state, yearly_price: e.target.value.replace(/[^\d.]/, '')}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className='basis-1/2'>
|
<div className='basis-1/2'>
|
||||||
<div className='flex justify-between'>
|
<div className='mb-1 flex h-6 items-center justify-between'>
|
||||||
<Heading level={6} grey>Add a free trial</Heading>
|
<Heading level={6}>Add a free trial</Heading>
|
||||||
<Toggle onChange={() => {}} />
|
<Toggle onChange={() => {}} />
|
||||||
</div>
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -111,6 +130,7 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
||||||
Members will be subscribed at full price once the trial ends. <a href="https://ghost.org/" rel="noreferrer" target="_blank">Learn more</a>
|
Members will be subscribed at full price once the trial ends. <a href="https://ghost.org/" rel="noreferrer" target="_blank">Learn more</a>
|
||||||
</>}
|
</>}
|
||||||
placeholder='0'
|
placeholder='0'
|
||||||
|
rightPlaceholder='days'
|
||||||
value={formState.trial_days}
|
value={formState.trial_days}
|
||||||
disabled
|
disabled
|
||||||
onChange={e => updateForm(state => ({...state, trial_days: e.target.value.replace(/^[\d.]/, '')}))}
|
onChange={e => updateForm(state => ({...state, trial_days: e.target.value.replace(/^[\d.]/, '')}))}
|
||||||
|
|
Loading…
Add table
Reference in a new issue