feat: from marketplace
parent
39a6f0943d
commit
e2fec587f8
@ -0,0 +1,56 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiMoreFill } from '@remixicon/react'
|
||||||
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
|
// import Button from '@/app/components/base/button'
|
||||||
|
import {
|
||||||
|
PortalToFollowElem,
|
||||||
|
PortalToFollowElemContent,
|
||||||
|
PortalToFollowElemTrigger,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
}
|
||||||
|
|
||||||
|
const OperationDropdown: FC<Props> = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [open, doSetOpen] = useState(false)
|
||||||
|
const openRef = useRef(open)
|
||||||
|
const setOpen = useCallback((v: boolean) => {
|
||||||
|
doSetOpen(v)
|
||||||
|
openRef.current = v
|
||||||
|
}, [doSetOpen])
|
||||||
|
|
||||||
|
const handleTrigger = useCallback(() => {
|
||||||
|
setOpen(!openRef.current)
|
||||||
|
}, [setOpen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
placement='bottom-end'
|
||||||
|
offset={{
|
||||||
|
mainAxis: 0,
|
||||||
|
crossAxis: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||||
|
<ActionButton className={cn(open && 'bg-state-base-hover')}>
|
||||||
|
<RiMoreFill className='w-4 h-4 text-components-button-secondary-accent-text' />
|
||||||
|
</ActionButton>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-50'>
|
||||||
|
<div className='w-[112px] p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
|
||||||
|
<div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('common.operation.download')}</div>
|
||||||
|
{/* Wait marketplace */}
|
||||||
|
{/* <div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('common.operation.viewDetail')}</div> */}
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(OperationDropdown)
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Action from './action'
|
||||||
|
import type { Plugin } from '@/app/components/plugins/types.ts'
|
||||||
|
import I18n from '@/context/i18n'
|
||||||
|
|
||||||
|
import { formatNumber } from '@/utils/format'
|
||||||
|
|
||||||
|
enum ActionType {
|
||||||
|
install = 'install',
|
||||||
|
download = 'download',
|
||||||
|
// viewDetail = 'viewDetail', // wait for marketplace api
|
||||||
|
}
|
||||||
|
type Props = {
|
||||||
|
payload: Plugin
|
||||||
|
onAction: (type: ActionType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Item: FC<Props> = ({
|
||||||
|
payload,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { locale } = useContext(I18n)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex rounded-lg py-2 pr-1 pl-3 hover:bg-state-base-hover'>
|
||||||
|
<div
|
||||||
|
className='shrink-0 relative w-6 h-6 border-[0.5px] border-components-panel-border-subtle rounded-md bg-center bg-no-repeat bg-contain'
|
||||||
|
style={{ backgroundImage: `url(${payload.icon})` }}
|
||||||
|
/>
|
||||||
|
<div className='ml-2 w-0 grow flex'>
|
||||||
|
<div className='w-0 grow'>
|
||||||
|
<div className='h-4 leading-4 text-text-primary system-sm-medium truncate '>{payload.label[locale]}</div>
|
||||||
|
<div className='h-5 leading-5 text-text-tertiary system-xs-regular truncate'>{payload.brief[locale]}</div>
|
||||||
|
<div className='flex text-text-tertiary system-xs-regular space-x-1'>
|
||||||
|
<div>{payload.org}</div>
|
||||||
|
<div>·</div>
|
||||||
|
<div>{t('plugin.install', { num: formatNumber(payload.install_count || 0) })}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Action */}
|
||||||
|
<div className='flex items-center space-x-1 h-4 text-components-button-secondary-accent-text system-xs-medium'>
|
||||||
|
<div className='px-1.5'>{t('plugin.installAction')}</div>
|
||||||
|
<Action />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(Item)
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Item from './item'
|
||||||
|
import type { Plugin } from '@/app/components/plugins/types.ts'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
list: Plugin[]
|
||||||
|
// onInstall: () =>
|
||||||
|
}
|
||||||
|
|
||||||
|
const List: FC<Props> = ({
|
||||||
|
list,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='pt-3 px-4 py-1 text-text-primary system-sm-medium'>
|
||||||
|
{t('plugin.fromMarketplace')}
|
||||||
|
</div>
|
||||||
|
<div className='p-1'>
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<Item
|
||||||
|
key={index}
|
||||||
|
payload={item}
|
||||||
|
onAction={() => { }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(List)
|
||||||
Loading…
Reference in New Issue