chore: implement message UI

This commit is contained in:
molvqingtai 2023-07-27 13:42:49 +08:00
parent 5bb773c0e3
commit 32e3b43bc4
10 changed files with 221 additions and 22 deletions

View file

@ -87,14 +87,17 @@
"*.{js,jsx,ts,tsx}": "eslint --fix" "*.{js,jsx,ts,tsx}": "eslint --fix"
}, },
"dependencies": { "dependencies": {
"@perfsee/jsonr": "^1.8.2",
"@radix-ui/react-avatar": "^1.0.3", "@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-hover-card": "^1.0.6", "@radix-ui/react-hover-card": "^1.0.6",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.6.1", "class-variance-authority": "^0.6.1",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"date-fns": "^2.30.0",
"lucide-react": "^0.263.0", "lucide-react": "^0.263.0",
"peerjs": "^1.4.7", "peerjs": "^1.4.7",
"react-nice-avatar": "^1.4.1",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"tailwind-merge": "^1.13.2", "tailwind-merge": "^1.13.2",
"type-fest": "^3.13.0" "type-fest": "^3.13.0"

View file

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
dependencies: dependencies:
'@perfsee/jsonr':
specifier: ^1.8.2
version: 1.8.2
'@radix-ui/react-avatar': '@radix-ui/react-avatar':
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) version: 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
@ -23,12 +26,18 @@ dependencies:
clsx: clsx:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
date-fns:
specifier: ^2.30.0
version: 2.30.0
lucide-react: lucide-react:
specifier: ^0.263.0 specifier: ^0.263.0
version: 0.263.0(react@18.2.0) version: 0.263.0(react@18.2.0)
peerjs: peerjs:
specifier: ^1.4.7 specifier: ^1.4.7
version: 1.4.7 version: 1.4.7
react-nice-avatar:
specifier: ^1.4.1
version: 1.4.1(react@18.2.0)
react-use: react-use:
specifier: ^17.4.0 specifier: ^17.4.0
version: 17.4.0(react-dom@18.2.0)(react@18.2.0) version: 17.4.0(react-dom@18.2.0)(react@18.2.0)
@ -1034,6 +1043,12 @@ packages:
fastq: 1.15.0 fastq: 1.15.0
dev: true dev: true
/@perfsee/jsonr@1.8.2:
resolution: {integrity: sha512-16VkW9j0aH1MjKit7iD+X5uDcYMrEhAN8kE/wrvAJJ1b8KkCbivPU3bpDqszfjOGt8Hae21ukoz2+qPYSnqoqw==}
dependencies:
tslib: 2.6.0
dev: false
/@pkgjs/parseargs@0.11.0: /@pkgjs/parseargs@0.11.0:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -2440,6 +2455,10 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/chroma-js@2.4.2:
resolution: {integrity: sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==}
dev: false
/chrome-launcher@0.15.1: /chrome-launcher@0.15.1:
resolution: {integrity: sha512-UugC8u59/w2AyX5sHLZUHoxBAiSiunUhZa3zZwMH6zPVis0C3dDKiRWyUGIo14tTbZHGVviWxv3PQWZ7taZ4fg==} resolution: {integrity: sha512-UugC8u59/w2AyX5sHLZUHoxBAiSiunUhZa3zZwMH6zPVis0C3dDKiRWyUGIo14tTbZHGVviWxv3PQWZ7taZ4fg==}
engines: {node: '>=12.13.0'} engines: {node: '>=12.13.0'}
@ -2843,6 +2862,13 @@ packages:
engines: {node: '>= 12'} engines: {node: '>= 12'}
dev: true dev: true
/date-fns@2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
dependencies:
'@babel/runtime': 7.21.0
dev: false
/debounce@1.2.1: /debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
dev: true dev: true
@ -5489,7 +5515,6 @@ packages:
/object-assign@4.1.1: /object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true
/object-hash@3.0.0: /object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
@ -6066,7 +6091,6 @@ packages:
loose-envify: 1.4.0 loose-envify: 1.4.0
object-assign: 4.1.1 object-assign: 4.1.1
react-is: 16.13.1 react-is: 16.13.1
dev: true
/proto-list@1.2.4: /proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
@ -6154,7 +6178,17 @@ packages:
/react-is@16.13.1: /react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
/react-nice-avatar@1.4.1(react@18.2.0):
resolution: {integrity: sha512-IKC51UTBbPm7rVsFNCiSEs/oP7W2/scod7/s8xCxqF/yMGS+Xs5HDAVxpyTEwv2iCYVoBq8H/E8SnbqwnWvupA==}
peerDependencies:
react: '>=16.0.0'
dependencies:
'@babel/runtime': 7.21.0
chroma-js: 2.4.2
prop-types: 15.8.1
react: 18.2.0
dev: false
/react-refresh@0.14.0: /react-refresh@0.14.0:
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}

View file

@ -6,7 +6,7 @@ export interface AppContainerProps {
const AppContainer: FC<AppContainerProps> = ({ children }) => { const AppContainer: FC<AppContainerProps> = ({ children }) => {
return ( return (
<div className="fixed bottom-10 right-10 top-10 z-top box-border grid w-1/4 grid-flow-col grid-rows-[auto_1fr_auto] overflow-hidden rounded-xl bg-white shadow-2xl transition-transform"> <div className="fixed bottom-10 right-10 top-10 z-top box-border grid w-1/4 grid-flow-col grid-rows-[auto_1fr_auto] overflow-hidden rounded-xl bg-slate-50 shadow-2xl transition-transform">
{children} {children}
</div> </div>
) )

View file

@ -1,7 +1,7 @@
import { useState, type FC, type ChangeEvent } from 'react' import { useState, type FC, type ChangeEvent } from 'react'
import { Textarea } from '@/components/ui/Textarea' import { Textarea } from '@/components/ui/Textarea'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Smile, Command, CornerDownLeft } from 'lucide-react' import { SmileIcon, CommandIcon, CornerDownLeftIcon } from 'lucide-react'
import { useBreakpoint } from '@/hooks/useBreakpoint' import { useBreakpoint } from '@/hooks/useBreakpoint'
const Footer: FC = () => { const Footer: FC = () => {
@ -13,22 +13,22 @@ const Footer: FC = () => {
} }
return ( return (
<div className="grid grid-cols-2 gap-y-2 p-4"> <div className="grid grid-cols-2 gap-y-2 p-4">
<Textarea <Textarea
className="col-span-2" className="col-span-2 rounded-lg bg-gray-50"
rows={is2XL ? 3 : 2} rows={is2XL ? 3 : 2}
value={message} value={message}
placeholder="Type your message here." placeholder="Type your message here."
onInput={handleInput} onInput={handleInput}
/> />
<Button variant="ghost" size="sm" className="place-self-start"> <Button variant="ghost" size="icon" className="place-self-start">
<Smile size={20} /> <SmileIcon size={20} />
</Button> </Button>
<Button size="sm" className="place-self-end"> <Button size="sm" className="place-self-end">
<span className="mr-2">Send</span> <span className="mr-2">Send</span>
<Command className="text-slate-400" size={12}></Command> <CommandIcon className="text-slate-400" size={12}></CommandIcon>
<CornerDownLeft className="text-slate-400" size={12}></CornerDownLeft> <CornerDownLeftIcon className="text-slate-400" size={12}></CornerDownLeftIcon>
</Button> </Button>
</div> </div>
) )

View file

@ -1,18 +1,18 @@
import { useState, type FC } from 'react' import { type FC } from 'react'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/HoverCard' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/HoverCard'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import getWebSiteInfo from '@/utils/getWebsiteInfo' import getWebSiteInfo from '@/utils/getWebsiteInfo'
const Header: FC = ({ ...props }) => { const Header: FC = () => {
const [websiteInfo] = useState(getWebSiteInfo()) const websiteInfo = getWebSiteInfo()
return ( return (
<div className="flex h-12 items-center px-4 shadow-sm 2xl:h-14"> <div className="shadow-xs flex h-12 items-center bg-white px-4 2xl:h-14">
<img className="h-8 w-8 overflow-hidden rounded-full" src={websiteInfo.icon} /> <img className="h-8 w-8 overflow-hidden rounded-full" src={websiteInfo.icon} />
<HoverCard> <HoverCard>
<HoverCardTrigger asChild> <HoverCardTrigger asChild>
<Button className="overflow-hidden text-xl" variant="link"> <Button className="overflow-hidden" variant="link">
<h1 className="truncate">{websiteInfo.hostname}</h1> <span className="truncate text-lg font-medium text-slate-600">{websiteInfo.hostname}</span>
</Button> </Button>
</HoverCardTrigger> </HoverCardTrigger>
<HoverCardContent className="w-80"> <HoverCardContent className="w-80">

View file

@ -0,0 +1,45 @@
import { FrownIcon, type LucideIcon, ThumbsUpIcon } from 'lucide-react'
import { type MouseEvent, type FC } from 'react'
import { Button } from '@/components/ui/Button'
import { cn } from '@/utils'
export interface LikeButtonProps {
type: 'like' | 'hate'
count: number
checked: boolean
onClick?: (e: MouseEvent<HTMLButtonElement>) => void
onChange?: (checked: boolean, count: number) => void
}
const iconMapping: Record<LikeButtonProps['type'], LucideIcon> = {
like: ThumbsUpIcon,
hate: FrownIcon
}
const LikeButton: FC<LikeButtonProps> = ({ type, checked, count, onClick, onChange }) => {
const Icon = iconMapping[type]
const handleOnClick = (e: MouseEvent<HTMLButtonElement>) => {
onClick?.(e)
onChange?.(!checked, checked ? count - 1 : count + 1)
}
return (
<Button
onClick={handleOnClick}
variant="secondary"
className={cn(
'flex items-center gap-x-1 rounded-full transition-all ',
checked ? 'text-orange-500' : 'text-slate-500'
)}
size="xs"
>
<Icon size={14} />
{!!count && <span className="text-xs">{count}</span>}
</Button>
)
}
LikeButton.displayName = 'LikeButton'
export default LikeButton

View file

@ -1,7 +1,71 @@
import { type FC } from 'react' import { type FC, useState } from 'react'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar'
import { format } from 'date-fns'
const Message: FC = () => { import LikeButton from './LikeButton'
return <div>Message</div> export interface MessageProps {
data: {
id: string
body: string
username: string
avatar: string
date: number
likeChecked: boolean
hateChecked: boolean
likeCount: number
hateCount: number
}
}
const Message: FC<MessageProps> = ({ data }) => {
const [formatData, setFormatData] = useState({
...data,
date: format(data.date, 'yyyy/MM/dd HH:mm:ss')
})
const handleLikeChange = (type: 'like' | 'hate', checked: boolean, count: number) => {
setFormatData((prev) => {
return {
...prev,
[`${type}Checked`]: checked,
[`${type}Count`]: count
}
})
}
return (
<div className="flex w-full gap-x-2">
<Avatar>
<AvatarImage src={formatData.avatar} />
<AvatarFallback>{formatData.username}</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-baseline gap-x-2 leading-none">
<div className="text-sm font-medium text-slate-600">{formatData.username}</div>
<div className="text-xs text-slate-400">{formatData.date}</div>
</div>
<div>
<div className="pb-2">
<pre className="text-sm">{formatData.body}</pre>
</div>
<div className="flex justify-end gap-x-2 leading-none">
<LikeButton
type="like"
checked={formatData.likeChecked}
onChange={(...args) => handleLikeChange('like', ...args)}
count={formatData.likeCount}
></LikeButton>
<LikeButton
type="hate"
checked={formatData.hateChecked}
onChange={(...args) => handleLikeChange('hate', ...args)}
count={formatData.hateCount}
></LikeButton>
</div>
</div>
</div>
</div>
)
} }
Message.displayName = 'Message' Message.displayName = 'Message'

View file

@ -1,7 +1,49 @@
import { type FC } from 'react' import { type FC } from 'react'
import Message from './Message'
const Main: FC = () => { const Main: FC = () => {
return <div>Main</div> const messages = [
{
id: '1',
body: 'Who are you?',
username: 'molvqingtai',
avatar: 'https://github.com/shadcn.png',
date: Date.now(),
likeChecked: false,
hateChecked: false,
likeCount: 0,
hateCount: 0
},
{
id: '2',
body: `I'm Chinese`,
username: 'Love XJP',
avatar: 'https://github.com/shadcn.png',
date: Date.now(),
likeChecked: false,
hateChecked: false,
likeCount: 0,
hateCount: 0
},
{
id: '1',
body: 'Do you like XJP?',
username: 'molvqingtai',
avatar: 'https://github.com/shadcn.png',
date: Date.now(),
likeChecked: false,
hateChecked: false,
likeCount: 98,
hateCount: 2
}
]
return (
<div className="flex flex-col gap-y-4 p-4">
{messages.map((message) => (
<Message key={message.id} data={message} />
))}
</div>
)
} }
Main.displayName = 'Main' Main.displayName = 'Main'

View file

@ -19,6 +19,7 @@ const buttonVariants = cva(
size: { size: {
default: 'h-9 px-4 py-2', default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs', sm: 'h-8 rounded-md px-3 text-xs',
xs: 'h-6 rounded-md px-2 text-xs',
lg: 'h-10 rounded-md px-8', lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9' icon: 'h-9 w-9'
} }

View file

@ -1,4 +1,14 @@
const getWebSiteInfo = () => { export interface WebSiteInfo {
host: string
hostname: string
href: string
origin: string
title: string
icon: string
description: string
}
const getWebSiteInfo = (): WebSiteInfo => {
return { return {
host: document.location.host, host: document.location.host,
hostname: document.location.hostname, hostname: document.location.hostname,