chore: implement message UI
This commit is contained in:
parent
5bb773c0e3
commit
32e3b43bc4
10 changed files with 221 additions and 22 deletions
|
@ -87,14 +87,17 @@
|
|||
"*.{js,jsx,ts,tsx}": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@perfsee/jsonr": "^1.8.2",
|
||||
"@radix-ui/react-avatar": "^1.0.3",
|
||||
"@radix-ui/react-hover-card": "^1.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"class-variance-authority": "^0.6.1",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"lucide-react": "^0.263.0",
|
||||
"peerjs": "^1.4.7",
|
||||
"react-nice-avatar": "^1.4.1",
|
||||
"react-use": "^17.4.0",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"type-fest": "^3.13.0"
|
||||
|
|
|
@ -5,6 +5,9 @@ settings:
|
|||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@perfsee/jsonr':
|
||||
specifier: ^1.8.2
|
||||
version: 1.8.2
|
||||
'@radix-ui/react-avatar':
|
||||
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)
|
||||
|
@ -23,12 +26,18 @@ dependencies:
|
|||
clsx:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
date-fns:
|
||||
specifier: ^2.30.0
|
||||
version: 2.30.0
|
||||
lucide-react:
|
||||
specifier: ^0.263.0
|
||||
version: 0.263.0(react@18.2.0)
|
||||
peerjs:
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.7
|
||||
react-nice-avatar:
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.1(react@18.2.0)
|
||||
react-use:
|
||||
specifier: ^17.4.0
|
||||
version: 17.4.0(react-dom@18.2.0)(react@18.2.0)
|
||||
|
@ -1034,6 +1043,12 @@ packages:
|
|||
fastq: 1.15.0
|
||||
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:
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
@ -2440,6 +2455,10 @@ packages:
|
|||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/chroma-js@2.4.2:
|
||||
resolution: {integrity: sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==}
|
||||
dev: false
|
||||
|
||||
/chrome-launcher@0.15.1:
|
||||
resolution: {integrity: sha512-UugC8u59/w2AyX5sHLZUHoxBAiSiunUhZa3zZwMH6zPVis0C3dDKiRWyUGIo14tTbZHGVviWxv3PQWZ7taZ4fg==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
|
@ -2843,6 +2862,13 @@ packages:
|
|||
engines: {node: '>= 12'}
|
||||
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:
|
||||
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||
dev: true
|
||||
|
@ -5489,7 +5515,6 @@ packages:
|
|||
/object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/object-hash@3.0.0:
|
||||
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||
|
@ -6066,7 +6091,6 @@ packages:
|
|||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
dev: true
|
||||
|
||||
/proto-list@1.2.4:
|
||||
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
|
||||
|
@ -6154,7 +6178,17 @@ packages:
|
|||
|
||||
/react-is@16.13.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
|
||||
|
|
|
@ -6,7 +6,7 @@ export interface AppContainerProps {
|
|||
|
||||
const AppContainer: FC<AppContainerProps> = ({ children }) => {
|
||||
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}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, type FC, type ChangeEvent } from 'react'
|
||||
import { Textarea } from '@/components/ui/Textarea'
|
||||
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'
|
||||
|
||||
const Footer: FC = () => {
|
||||
|
@ -13,22 +13,22 @@ const Footer: FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-y-2 p-4">
|
||||
<div className="grid grid-cols-2 gap-y-2 p-4">
|
||||
<Textarea
|
||||
className="col-span-2"
|
||||
className="col-span-2 rounded-lg bg-gray-50"
|
||||
rows={is2XL ? 3 : 2}
|
||||
value={message}
|
||||
placeholder="Type your message here."
|
||||
onInput={handleInput}
|
||||
/>
|
||||
|
||||
<Button variant="ghost" size="sm" className="place-self-start">
|
||||
<Smile size={20} />
|
||||
<Button variant="ghost" size="icon" className="place-self-start">
|
||||
<SmileIcon size={20} />
|
||||
</Button>
|
||||
<Button size="sm" className="place-self-end">
|
||||
<span className="mr-2">Send</span>
|
||||
<Command className="text-slate-400" size={12}></Command>
|
||||
<CornerDownLeft className="text-slate-400" size={12}></CornerDownLeft>
|
||||
<CommandIcon className="text-slate-400" size={12}></CommandIcon>
|
||||
<CornerDownLeftIcon className="text-slate-400" size={12}></CornerDownLeftIcon>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { useState, type FC } from 'react'
|
||||
import { type FC } from 'react'
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/HoverCard'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import getWebSiteInfo from '@/utils/getWebsiteInfo'
|
||||
|
||||
const Header: FC = ({ ...props }) => {
|
||||
const [websiteInfo] = useState(getWebSiteInfo())
|
||||
const Header: FC = () => {
|
||||
const websiteInfo = getWebSiteInfo()
|
||||
|
||||
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} />
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button className="overflow-hidden text-xl" variant="link">
|
||||
<h1 className="truncate">{websiteInfo.hostname}</h1>
|
||||
<Button className="overflow-hidden" variant="link">
|
||||
<span className="truncate text-lg font-medium text-slate-600">{websiteInfo.hostname}</span>
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-80">
|
||||
|
|
45
src/components/Main/LikeButton.tsx
Normal file
45
src/components/Main/LikeButton.tsx
Normal 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
|
|
@ -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 = () => {
|
||||
return <div>Message</div>
|
||||
import LikeButton from './LikeButton'
|
||||
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'
|
||||
|
|
|
@ -1,7 +1,49 @@
|
|||
import { type FC } from 'react'
|
||||
import Message from './Message'
|
||||
|
||||
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'
|
||||
|
|
|
@ -19,6 +19,7 @@ const buttonVariants = cva(
|
|||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
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',
|
||||
icon: 'h-9 w-9'
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
host: document.location.host,
|
||||
hostname: document.location.hostname,
|
||||
|
|
Loading…
Reference in a new issue