refactor: refactoring MessageInput
This commit is contained in:
parent
129129c455
commit
558540a361
11 changed files with 338 additions and 60 deletions
|
@ -100,6 +100,10 @@
|
||||||
"react-nice-avatar": "^1.4.1",
|
"react-nice-avatar": "^1.4.1",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
|
"remesh": "^4.2.0",
|
||||||
|
"remesh-logger": "^4.1.0",
|
||||||
|
"remesh-react": "^4.1.0",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
"tailwind-merge": "^1.13.2",
|
"tailwind-merge": "^1.13.2",
|
||||||
"type-fest": "^3.13.0"
|
"type-fest": "^3.13.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -53,6 +53,18 @@ dependencies:
|
||||||
remark-gfm:
|
remark-gfm:
|
||||||
specifier: ^3.0.1
|
specifier: ^3.0.1
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
|
remesh:
|
||||||
|
specifier: ^4.2.0
|
||||||
|
version: 4.2.0(rxjs@7.8.1)
|
||||||
|
remesh-logger:
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0(remesh@4.2.0)
|
||||||
|
remesh-react:
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0(react-dom@18.2.0)(react@18.2.0)(remesh@4.2.0)(rxjs@7.8.1)
|
||||||
|
rxjs:
|
||||||
|
specifier: ^7.8.1
|
||||||
|
version: 7.8.1
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^1.13.2
|
specifier: ^1.13.2
|
||||||
version: 1.13.2
|
version: 1.13.2
|
||||||
|
@ -4527,6 +4539,11 @@ packages:
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/is-plain-object@5.0.0:
|
||||||
|
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/is-regex@1.1.4:
|
/is-regex@1.1.4:
|
||||||
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
|
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
@ -6726,6 +6743,53 @@ packages:
|
||||||
unified: 10.1.2
|
unified: 10.1.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/remesh-debugger-helper@4.1.0(remesh@4.2.0):
|
||||||
|
resolution: {integrity: sha512-nBbfznOEuu9Z1tpLIBOKEYcYrQvOCc+JyyPWYRG3H79JO/jxIUEAGg6YzAw59EUE/nhMPyx6qwyB7exJwFsZug==}
|
||||||
|
peerDependencies:
|
||||||
|
remesh: ^4.0.0
|
||||||
|
dependencies:
|
||||||
|
remesh: 4.2.0(rxjs@7.8.1)
|
||||||
|
tslib: 2.6.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/remesh-logger@4.1.0(remesh@4.2.0):
|
||||||
|
resolution: {integrity: sha512-OEmmBFk6KVtv0JahVhh29JbcF5IeMhxqiBaz2UAqY5x5d8W6UJtYmQFat2/2HUYV/Y1C+ooGi/kzh3Ka6NoyMQ==}
|
||||||
|
peerDependencies:
|
||||||
|
remesh: ^4.0.0
|
||||||
|
dependencies:
|
||||||
|
remesh: 4.2.0(rxjs@7.8.1)
|
||||||
|
remesh-debugger-helper: 4.1.0(remesh@4.2.0)
|
||||||
|
tslib: 2.6.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/remesh-react@4.1.0(react-dom@18.2.0)(react@18.2.0)(remesh@4.2.0)(rxjs@7.8.1):
|
||||||
|
resolution: {integrity: sha512-VaTZ2y3A84tiPNxb2/wgDt6i65zSfsxlAP1sTSDC+5C5vnhTy8Q8O8AaYJMOKqXUDtUNI9ld9Qu0wqfdxig/dg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.10.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.10.0 || ^17.0.0 || ^18.0.0
|
||||||
|
remesh: ^4.0.0
|
||||||
|
rxjs: ^7.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
remesh: 4.2.0(rxjs@7.8.1)
|
||||||
|
rxjs: 7.8.1
|
||||||
|
tslib: 2.6.0
|
||||||
|
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/remesh@4.2.0(rxjs@7.8.1):
|
||||||
|
resolution: {integrity: sha512-t2xwtFYnEwrcWddxtEb/NF7kR+x62Er52gfk+sCa4HmaQC+J5zg41pYZz6TTl4PrDghs2et1tr1dYZpbYGAu6g==}
|
||||||
|
engines: {node: '>=14.x'}
|
||||||
|
peerDependencies:
|
||||||
|
rxjs: ^7.0.0
|
||||||
|
dependencies:
|
||||||
|
is-plain-object: 5.0.0
|
||||||
|
rxjs: 7.8.1
|
||||||
|
shallowequal: 1.1.0
|
||||||
|
tslib: 2.6.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/request@2.88.2:
|
/request@2.88.2:
|
||||||
resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}
|
resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -6894,7 +6958,6 @@ packages:
|
||||||
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
|
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.0
|
tslib: 2.6.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/sade@1.8.1:
|
/sade@1.8.1:
|
||||||
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
||||||
|
@ -7017,6 +7080,10 @@ packages:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/shallowequal@1.1.0:
|
||||||
|
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/shebang-command@1.2.0:
|
/shebang-command@1.2.0:
|
||||||
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
|
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -7921,6 +7988,14 @@ packages:
|
||||||
punycode: 2.3.0
|
punycode: 2.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/use-sync-external-store@1.2.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/util-deprecate@1.0.2:
|
/util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
|
|
@ -1,58 +1,57 @@
|
||||||
import { type FC, type ChangeEvent } from 'react'
|
import { type FC, type ChangeEvent, type KeyboardEvent } from 'react'
|
||||||
import { Textarea } from '@/components/ui/Textarea'
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
|
|
||||||
import { Markdown } from '@/components/ui/Markdown'
|
import { Markdown } from '@/components/ui/Markdown'
|
||||||
import { cn } from '@/utils'
|
import { cn } from '@/utils'
|
||||||
|
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
|
||||||
|
import MessageInputDomain from '@/domain/MessageInput'
|
||||||
|
import { MESSAGE_MAX_LENGTH } from '@/constants'
|
||||||
|
|
||||||
export interface MessageInputProps {
|
export interface MessageInputProps {
|
||||||
value?: string
|
|
||||||
focus?: boolean
|
|
||||||
preview?: boolean
|
|
||||||
className?: string
|
className?: string
|
||||||
maxLength?: number
|
maxLength?: number
|
||||||
onInput?: (message: string) => void
|
|
||||||
onChange?: (message: string) => void
|
|
||||||
onFocus?: () => void
|
|
||||||
onBlur?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageInput: FC<MessageInputProps> = ({
|
const MessageInput: FC<MessageInputProps> = ({ className }) => {
|
||||||
value,
|
const send = useRemeshSend()
|
||||||
focus = true,
|
const messageInputDomain = useRemeshDomain(MessageInputDomain())
|
||||||
className,
|
|
||||||
preview,
|
const message = useRemeshQuery(messageInputDomain.query.ValueQuery())
|
||||||
maxLength = 500,
|
const isPreview = useRemeshQuery(messageInputDomain.query.PreviewQuery())
|
||||||
onInput,
|
|
||||||
onChange,
|
const handleInput = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
onFocus,
|
send(messageInputDomain.command.InputCommand(e.target.value))
|
||||||
onBlur
|
}
|
||||||
}) => {
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && !(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey)) {
|
||||||
|
e.preventDefault()
|
||||||
|
send(messageInputDomain.command.EnterCommand())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative', className)}>
|
<div className={cn('relative', className)}>
|
||||||
{preview ? (
|
{isPreview ? (
|
||||||
<Markdown className="max-h-32 rounded-lg border border-input bg-gray-50 2xl:max-h-40">{value}</Markdown>
|
<Markdown className="max-h-32 rounded-lg border border-input bg-gray-50 2xl:max-h-40">{message}</Markdown>
|
||||||
) : (
|
) : (
|
||||||
// Hack: Auto-Growing Textarea
|
// Hack: Auto-Growing Textarea
|
||||||
<div
|
<div
|
||||||
data-value={value}
|
data-value={message}
|
||||||
className="grid after:pointer-events-none after:invisible after:col-start-1 after:col-end-2 after:row-start-1 after:row-end-2 after:box-border after:max-h-28 after:w-full after:overflow-x-hidden after:whitespace-pre-wrap after:break-words after:rounded-lg after:border after:px-3 after:py-2 after:pb-5 after:text-sm after:content-[attr(data-value)] after:2xl:max-h-40"
|
className="grid after:pointer-events-none after:invisible after:col-start-1 after:col-end-2 after:row-start-1 after:row-end-2 after:box-border after:max-h-28 after:w-full after:overflow-x-hidden after:whitespace-pre-wrap after:break-words after:rounded-lg after:border after:px-3 after:py-2 after:pb-5 after:text-sm after:content-[attr(data-value)] after:2xl:max-h-40"
|
||||||
>
|
>
|
||||||
<Textarea
|
<Textarea
|
||||||
autoFocus={focus}
|
onKeyDown={handleKeyDown}
|
||||||
maxLength={maxLength}
|
maxLength={MESSAGE_MAX_LENGTH}
|
||||||
className="col-start-1 col-end-2 row-start-1 row-end-2 box-border max-h-28 resize-none overflow-x-hidden break-words rounded-lg bg-gray-50 pb-5 text-sm 2xl:max-h-40"
|
className="col-start-1 col-end-2 row-start-1 row-end-2 box-border max-h-28 resize-none overflow-x-hidden break-words rounded-lg bg-gray-50 pb-5 text-sm 2xl:max-h-40"
|
||||||
rows={2}
|
rows={2}
|
||||||
value={value}
|
value={message}
|
||||||
placeholder="Type your message here."
|
placeholder="Type your message here."
|
||||||
onInput={(e: ChangeEvent<HTMLTextAreaElement>) => onInput?.(e.target.value)}
|
onInput={handleInput}
|
||||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => onChange?.(e.target.value)}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="absolute bottom-1 right-3 rounded-lg text-xs text-slate-400">
|
<div className="absolute bottom-1 right-3 rounded-lg text-xs text-slate-400">
|
||||||
{value?.length ?? 0}/{maxLength}
|
{message?.length ?? 0}/{MESSAGE_MAX_LENGTH}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,19 +1,14 @@
|
||||||
import { useState, type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { SmileIcon, CornerDownLeftIcon, ImageIcon } from 'lucide-react'
|
import { SmileIcon, CornerDownLeftIcon, ImageIcon } from 'lucide-react'
|
||||||
import MessageInput from './MessageInput'
|
import MessageInput from './MessageInput'
|
||||||
|
|
||||||
const Footer: FC = () => {
|
const Footer: FC = () => {
|
||||||
const [message, setMessage] = useState('')
|
const handleSend = () => {}
|
||||||
|
|
||||||
const handleInput = (value: string) => {
|
|
||||||
setMessage(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-y-2 p-4">
|
<div className="grid gap-y-2 p-4">
|
||||||
<MessageInput value={message} onInput={handleInput}></MessageInput>
|
<MessageInput></MessageInput>
|
||||||
<div className="grid grid-cols-[auto_auto_1fr] items-center justify-items-end">
|
<div className="grid grid-cols-[auto_auto_1fr] items-center justify-items-end">
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<SmileIcon size={20} />
|
<SmileIcon size={20} />
|
||||||
|
@ -21,7 +16,7 @@ const Footer: FC = () => {
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<ImageIcon size={20} />
|
<ImageIcon size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm">
|
<Button size="sm" onClick={handleSend}>
|
||||||
<span className="mr-2">Send</span>
|
<span className="mr-2">Send</span>
|
||||||
<CornerDownLeftIcon className="text-slate-400" size={12}></CornerDownLeftIcon>
|
<CornerDownLeftIcon className="text-slate-400" size={12}></CornerDownLeftIcon>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,24 +1,28 @@
|
||||||
import { FrownIcon, type LucideIcon, ThumbsUpIcon } from 'lucide-react'
|
import { type MouseEvent, type FC, type ReactElement } from 'react'
|
||||||
import { type MouseEvent, type FC } from 'react'
|
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { cn } from '@/utils'
|
import { cn } from '@/utils'
|
||||||
|
|
||||||
|
export interface LikeButtonIconProps {
|
||||||
|
children: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LikeButtonIcon: FC<LikeButtonIconProps> = ({ children }) => children
|
||||||
|
|
||||||
export interface LikeButtonProps {
|
export interface LikeButtonProps {
|
||||||
type: 'like' | 'hate'
|
|
||||||
count: number
|
count: number
|
||||||
checked: boolean
|
checked: boolean
|
||||||
onClick?: (e: MouseEvent<HTMLButtonElement>) => void
|
onClick?: (e: MouseEvent<HTMLButtonElement>) => void
|
||||||
onChange?: (checked: boolean, count: number) => void
|
onChange?: (checked: boolean, count: number) => void
|
||||||
|
children: ReactElement<LikeButtonIconProps>
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMapping: Record<LikeButtonProps['type'], LucideIcon> = {
|
const LikeButton: FC<LikeButtonProps> & { Icon: FC<LikeButtonIconProps> } = ({
|
||||||
like: ThumbsUpIcon,
|
checked,
|
||||||
hate: FrownIcon
|
count,
|
||||||
}
|
onClick,
|
||||||
|
onChange,
|
||||||
const LikeButton: FC<LikeButtonProps> = ({ type, checked, count, onClick, onChange }) => {
|
children
|
||||||
const Icon = iconMapping[type]
|
}) => {
|
||||||
|
|
||||||
const handleOnClick = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleOnClick = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
onClick?.(e)
|
onClick?.(e)
|
||||||
onChange?.(!checked, checked ? count - 1 : count + 1)
|
onChange?.(!checked, checked ? count - 1 : count + 1)
|
||||||
|
@ -35,12 +39,14 @@ const LikeButton: FC<LikeButtonProps> = ({ type, checked, count, onClick, onChan
|
||||||
)}
|
)}
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
<Icon size={14} />
|
{children}
|
||||||
{!!count && <span className="min-w-0 text-xs">{count}</span>}
|
{!!count && <span className="min-w-0 text-xs">{count}</span>}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LikeButton.Icon = LikeButtonIcon
|
||||||
|
|
||||||
LikeButton.displayName = 'LikeButton'
|
LikeButton.displayName = 'LikeButton'
|
||||||
|
|
||||||
export default LikeButton
|
export default LikeButton
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
|
|
||||||
import LikeButton from './LikeButton'
|
import LikeButton from './LikeButton'
|
||||||
|
import { FrownIcon, ThumbsUpIcon } from 'lucide-react'
|
||||||
export interface MessageProps {
|
export interface MessageProps {
|
||||||
data: {
|
data: {
|
||||||
id: string
|
id: string
|
||||||
|
@ -50,17 +51,23 @@ const Message: FC<MessageProps> = ({ data }) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-flow-col justify-end gap-x-2 leading-none">
|
<div className="grid grid-flow-col justify-end gap-x-2 leading-none">
|
||||||
<LikeButton
|
<LikeButton
|
||||||
type="like"
|
|
||||||
checked={formatData.likeChecked}
|
checked={formatData.likeChecked}
|
||||||
onChange={(...args) => handleLikeChange('like', ...args)}
|
onChange={(...args) => handleLikeChange('like', ...args)}
|
||||||
count={formatData.likeCount}
|
count={formatData.likeCount}
|
||||||
></LikeButton>
|
>
|
||||||
|
<LikeButton.Icon>
|
||||||
|
<ThumbsUpIcon size={14}></ThumbsUpIcon>
|
||||||
|
</LikeButton.Icon>
|
||||||
|
</LikeButton>
|
||||||
<LikeButton
|
<LikeButton
|
||||||
type="hate"
|
|
||||||
checked={formatData.hateChecked}
|
checked={formatData.hateChecked}
|
||||||
onChange={(...args) => handleLikeChange('hate', ...args)}
|
onChange={(...args) => handleLikeChange('hate', ...args)}
|
||||||
count={formatData.hateCount}
|
count={formatData.hateCount}
|
||||||
></LikeButton>
|
>
|
||||||
|
<LikeButton.Icon>
|
||||||
|
<FrownIcon size={14}></FrownIcon>
|
||||||
|
</LikeButton.Icon>
|
||||||
|
</LikeButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,5 +76,4 @@ const Message: FC<MessageProps> = ({ data }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
Message.displayName = 'Message'
|
Message.displayName = 'Message'
|
||||||
|
|
||||||
export default Message
|
export default Message
|
||||||
|
|
|
@ -32,7 +32,7 @@ const Main: FC = () => {
|
||||||
avatar: 'https://github.com/shadcn.png',
|
avatar: 'https://github.com/shadcn.png',
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
likeChecked: false,
|
likeChecked: false,
|
||||||
hateChecked: false,
|
hateChecked: true,
|
||||||
likeCount: 9999,
|
likeCount: 9999,
|
||||||
hateCount: 2
|
hateCount: 2
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,3 +15,5 @@ export const BREAKPOINTS = {
|
||||||
'2xl': '1536px'
|
'2xl': '1536px'
|
||||||
// => @media (min-width: 1536px) { ... }
|
// => @media (min-width: 1536px) { ... }
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export const MESSAGE_MAX_LENGTH = 500 as const
|
||||||
|
|
59
src/domain/MessageInput.ts
Normal file
59
src/domain/MessageInput.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { Remesh } from 'remesh'
|
||||||
|
import InputModule from './modules/Input'
|
||||||
|
|
||||||
|
const MessageInputDomain = Remesh.domain({
|
||||||
|
name: 'MessageInputDomain',
|
||||||
|
impl: (domain) => {
|
||||||
|
const inputModule = InputModule(domain, {
|
||||||
|
name: 'MessageInput'
|
||||||
|
})
|
||||||
|
|
||||||
|
const PreviewState = domain.state({
|
||||||
|
name: 'PreviewState',
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const PreviewQuery = domain.query({
|
||||||
|
name: 'PreviewQuery',
|
||||||
|
impl: ({ get }) => {
|
||||||
|
return get(PreviewState())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const PreviewCommand = domain.command({
|
||||||
|
name: 'PreviewCommand',
|
||||||
|
impl: (_, value: boolean) => {
|
||||||
|
return PreviewState().new(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const EnterEvent = domain.event({
|
||||||
|
name: 'EnterEvent'
|
||||||
|
})
|
||||||
|
|
||||||
|
const EnterCommand = domain.command({
|
||||||
|
name: 'EnterCommand',
|
||||||
|
impl: () => {
|
||||||
|
return EnterEvent()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: {
|
||||||
|
...inputModule.query,
|
||||||
|
PreviewQuery
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
...inputModule.command,
|
||||||
|
EnterCommand,
|
||||||
|
PreviewCommand
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
...inputModule.event,
|
||||||
|
EnterEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default MessageInputDomain
|
123
src/domain/modules/Input.ts
Normal file
123
src/domain/modules/Input.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import { Remesh, type Capitalize, type RemeshDomainContext } from 'remesh'
|
||||||
|
|
||||||
|
export interface InputModuleOptions {
|
||||||
|
name: Capitalize
|
||||||
|
value?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputModule = (domain: RemeshDomainContext, options: InputModuleOptions) => {
|
||||||
|
const ValueState = domain.state({
|
||||||
|
name: `${options.name}.ValueState`,
|
||||||
|
default: options.value ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const ValueQuery = domain.query({
|
||||||
|
name: `${options.name}.ValueQuery`,
|
||||||
|
impl: ({ get }) => {
|
||||||
|
return get(ValueState())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const InputEvent = domain.event<string>({
|
||||||
|
name: `${options.name}.InputEvent`
|
||||||
|
})
|
||||||
|
|
||||||
|
const InputCommand = domain.command({
|
||||||
|
name: `${options.name}.InputCommand`,
|
||||||
|
impl: (_, value: string) => {
|
||||||
|
InputEvent(value)
|
||||||
|
return ValueState().new(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ChangeEvent = domain.event<string>({
|
||||||
|
name: `${options.name}.ChangeEvent`
|
||||||
|
})
|
||||||
|
|
||||||
|
const ChangeCommand = domain.command({
|
||||||
|
name: `${options.name}.ChangeCommand`,
|
||||||
|
impl: (_, value: string) => {
|
||||||
|
ChangeEvent(value)
|
||||||
|
return ValueState().new(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const FocusState = domain.state({
|
||||||
|
name: `${options.name}.FocusState`,
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const FocusQuery = domain.query({
|
||||||
|
name: `${options.name}.FocusQuery`,
|
||||||
|
impl: ({ get }) => {
|
||||||
|
return get(FocusState())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const FocusEvent = domain.event<boolean>({
|
||||||
|
name: `${options.name}.FocusEvent`
|
||||||
|
})
|
||||||
|
|
||||||
|
const BlurEvent = domain.event<boolean>({
|
||||||
|
name: `${options.name}.BlurEvent`
|
||||||
|
})
|
||||||
|
|
||||||
|
const BlurCommand = domain.command({
|
||||||
|
name: `${options.name}.BlurCommand`,
|
||||||
|
impl: () => {
|
||||||
|
BlurEvent(false)
|
||||||
|
return FocusState().new(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const FocusCommand = domain.command({
|
||||||
|
name: `${options.name}.FocusCommand`,
|
||||||
|
impl: () => {
|
||||||
|
FocusEvent(true)
|
||||||
|
return FocusState().new(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const DisabledState = domain.state({
|
||||||
|
name: `${options.name}.DisabledState`,
|
||||||
|
default: options.disabled ?? false
|
||||||
|
})
|
||||||
|
|
||||||
|
const DisabledQuery = domain.query({
|
||||||
|
name: `${options.name}.DisabledQuery`,
|
||||||
|
impl: ({ get }) => {
|
||||||
|
return get(DisabledState())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const DisabledCommand = domain.command({
|
||||||
|
name: `${options.name}.DisabledCommand`,
|
||||||
|
impl: (_, value: boolean) => {
|
||||||
|
return DisabledState().new(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Remesh.module({
|
||||||
|
query: {
|
||||||
|
ValueQuery,
|
||||||
|
FocusQuery,
|
||||||
|
DisabledQuery
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
InputCommand,
|
||||||
|
ChangeCommand,
|
||||||
|
BlurCommand,
|
||||||
|
FocusCommand,
|
||||||
|
DisabledCommand
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
InputEvent,
|
||||||
|
ChangeEvent,
|
||||||
|
FocusEvent,
|
||||||
|
BlurEvent
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InputModule
|
11
src/main.tsx
11
src/main.tsx
|
@ -2,14 +2,23 @@ import React from 'react'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import createShadowRoot from './createShadowRoot'
|
import createShadowRoot from './createShadowRoot'
|
||||||
import style from './index.css?inline'
|
import style from './index.css?inline'
|
||||||
|
import { RemeshRoot } from 'remesh-react'
|
||||||
|
import { RemeshLogger } from 'remesh-logger'
|
||||||
|
import { Remesh } from 'remesh'
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
|
const store = Remesh.store({
|
||||||
|
inspectors: [RemeshLogger()]
|
||||||
|
})
|
||||||
|
|
||||||
createShadowRoot(__NAME__, {
|
createShadowRoot(__NAME__, {
|
||||||
style: __DEV__ ? '' : style,
|
style: __DEV__ ? '' : style,
|
||||||
mode: __DEV__ ? 'open' : 'closed'
|
mode: __DEV__ ? 'open' : 'closed'
|
||||||
}).render(
|
}).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<RemeshRoot store={store}>
|
||||||
|
<App />
|
||||||
|
</RemeshRoot>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue