diff --git a/public/locales/en/link-input-modal.json b/public/locales/en/link-input-modal.json index 9928583..7ebdfc8 100644 --- a/public/locales/en/link-input-modal.json +++ b/public/locales/en/link-input-modal.json @@ -11,5 +11,7 @@ "newTab": "Open in new tab", "currentTab": "Open in current window", "cancel": "Cancel", - "confirm": "Confirm" + "confirm": "Confirm", + "displayRequiredError": "Link Display Text is required", + "urlRequiredError": "Link URL is required" } diff --git a/public/locales/zh-TW/link-input-modal.json b/public/locales/zh-TW/link-input-modal.json index 80bb129..8ac1273 100644 --- a/public/locales/zh-TW/link-input-modal.json +++ b/public/locales/zh-TW/link-input-modal.json @@ -11,5 +11,7 @@ "newTab": "在新分頁中開啟", "currentTab": "在當前視窗中開啟", "cancel": "取消", - "confirm": "確認" + "confirm": "確認", + "displayRequiredError": "文字連結標題為必填", + "urlRequiredError": "文字連結 URL 為必填" } diff --git a/src/components/core/radio-group.js b/src/components/core/radio-group.js new file mode 100644 index 0000000..0fa5e5f --- /dev/null +++ b/src/components/core/radio-group.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; + +const RadioGroup = ({ name, label, options, value, onChange }) => { + return ( +
+ {label && {label}} +
+ {options.map((option) => ( +
+ +
+ ))} +
+
+ ); +}; + +RadioGroup.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string, + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + disabled: PropTypes.bool, + }) + ).isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default RadioGroup; diff --git a/src/components/core/text-input.js b/src/components/core/text-input.js new file mode 100644 index 0000000..2c70c9c --- /dev/null +++ b/src/components/core/text-input.js @@ -0,0 +1,78 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; + +const TextInput = React.forwardRef( + ( + { + id, + type = 'text', + value, + onChange, + placeholder, + disabled = false, + error, + required, + label, + hint, + className, + ...props + }, + ref + ) => { + return ( +
+ {label && ( + + )} + + {error && ( + + {error} + + )} +
+ ); + } +); + +TextInput.displayName = 'TextInput'; + +TextInput.propTypes = { + id: PropTypes.string, + type: PropTypes.string, + value: PropTypes.string, + onChange: PropTypes.func, + placeholder: PropTypes.string, + disabled: PropTypes.bool, + error: PropTypes.string, + required: PropTypes.bool, + label: PropTypes.string, + hint: PropTypes.string, + className: PropTypes.string, +}; + +export default TextInput; diff --git a/src/components/link-input-modal.js b/src/components/link-input-modal.js index 0d2142a..62cca42 100644 --- a/src/components/link-input-modal.js +++ b/src/components/link-input-modal.js @@ -2,12 +2,16 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from '@/lib/i18n'; import BasicModal from '@/components/core/modal/basic-modal'; +import TextInput from '@/components/core/text-input'; +import RadioGroup from '@/components/core/radio-group'; const LinkInputModal = ({ isOpen, onClose, onConfirm }) => { const [display, setDisplay] = useState(''); const [title, setTitle] = useState(''); const [url, setUrl] = useState(''); const [openInNewTab, setOpenInNewTab] = useState(true); + const [displayError, setDisplayError] = useState(''); + const [urlError, setUrlError] = useState(''); const t = useTranslation('link-input-modal'); const resetForm = () => { @@ -15,6 +19,8 @@ const LinkInputModal = ({ isOpen, onClose, onConfirm }) => { setTitle(''); setUrl(''); setOpenInNewTab(true); + setDisplayError(''); + setUrlError(''); }; const handleClose = () => { @@ -23,7 +29,13 @@ const LinkInputModal = ({ isOpen, onClose, onConfirm }) => { }; const handleConfirm = () => { - if (!display.trim() || !url.trim()) return; + const isDisplayEmpty = !display.trim(); + const isUrlEmpty = !url.trim(); + + if (isDisplayEmpty) setDisplayError(t('displayRequiredError')); + if (isUrlEmpty) setUrlError(t('urlRequiredError')); + if (isDisplayEmpty || isUrlEmpty) return; + const prefix = openInNewTab ? '@' : ''; const titlePart = title.trim() ? `[[${title.trim()}]]` : ''; const markdown = `${prefix}[${display.trim()}]${titlePart}(${url.trim()})`; @@ -42,76 +54,48 @@ const LinkInputModal = ({ isOpen, onClose, onConfirm }) => { confirmLabel={t('confirm')} >
-
- - setDisplay(e.target.value)} - placeholder={t('displayPlaceholder')} - className="w-full border border-border-main rounded-lg px-4 py-3 text-base text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary" - aria-required="true" - /> -
-
- - setTitle(e.target.value)} - placeholder={t('titlePlaceholder')} - className="w-full border border-border-main rounded-lg px-4 py-3 text-base text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary" - /> -
-
- - setUrl(e.target.value)} - placeholder={t('urlPlaceholder')} - className="w-full border border-border-main rounded-lg px-4 py-3 text-base text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-primary" - aria-required="true" - /> -
-
- {t('openMethod')} -
- - -
-
+ { + setDisplay(e.target.value); + setDisplayError(''); + }} + placeholder={t('displayPlaceholder')} + error={displayError} + required + /> + setTitle(e.target.value)} + placeholder={t('titlePlaceholder')} + /> + { + setUrl(e.target.value); + setUrlError(''); + }} + placeholder={t('urlPlaceholder')} + error={urlError} + required + /> + setOpenInNewTab(val === 'new-tab')} + />
);