feat: add domain to watchlist FAB

This commit is contained in:
vinceh121 2025-11-01 22:59:49 +01:00
parent c3832f06c3
commit 841e8dcba6
No known key found for this signature in database
GPG Key ID: 780725DCACF96F16
4 changed files with 162 additions and 36 deletions

View File

@ -0,0 +1,78 @@
import React, {useEffect, useState} from "react"
import {Flex, Modal, ModalProps, Select, Tag, Typography} from "antd"
import {getWatchlists, Watchlist} from "../../../utils/api"
import {t} from 'ttag'
import {DomainToTag} from "../../../utils/functions/DomainToTag"
function WatchlistOption({watchlist}: {watchlist: Watchlist}) {
return <Flex vertical>
<Typography.Text strong>{watchlist.name}</Typography.Text>
<Flex wrap>
{watchlist.domains.map(d => <DomainToTag link={false} domain={d} key={d.ldhName} />)}
</Flex>
</Flex>
}
interface WatchlistSelectionModalProps {
onFinish: (watchlist: Watchlist) => Promise<void>|void
description?: string
open?: boolean
modalProps?: Partial<ModalProps>
}
export default function WatchlistSelectionModal(props: WatchlistSelectionModalProps) {
const [watchlists, setWatchlists] = useState<Watchlist[] | undefined>()
const [selectedWatchlist, setSelectedWatchlist] = useState<Watchlist | undefined>()
const [validationLoading, setValidationLoading] = useState(false)
useEffect(() => {
getWatchlists().then(list => setWatchlists(list["hydra:member"]))
}, [])
const onFinish = () => {
const promise = props.onFinish(selectedWatchlist as Watchlist)
if (promise) {
setValidationLoading(true)
promise.finally(() => {
setSelectedWatchlist(undefined)
setValidationLoading(false)
})
} else {
setSelectedWatchlist(undefined)
}
}
return <Modal
open={props.open}
onOk={onFinish}
okButtonProps={{
disabled: !selectedWatchlist,
loading: validationLoading,
}}
{...props.modalProps ?? {}}
>
<Flex vertical>
<Typography.Text>
{
props.description
|| t`Select one of your available watchlists`
}
</Typography.Text>
<Select
placeholder={t`Watchlist`}
style={{width: '100%'}}
onChange={(_, option) => setSelectedWatchlist(option as Watchlist)}
options={watchlists}
value={selectedWatchlist?.token}
fieldNames={{
label: 'name',
value: 'token',
}}
loading={!watchlists}
status={selectedWatchlist ? '' : 'error'}
optionRender={(watchlist) => <WatchlistOption watchlist={watchlist.data}/>}
/>
</Flex>
</Modal>
}

View File

@ -1,8 +1,10 @@
import React, {useEffect, useState} from 'react' import React, {useEffect, useState} from 'react'
import type { FormProps} from 'antd' import type {FormProps} from 'antd'
import {Tooltip} from 'antd'
import {FloatButton} from 'antd'
import {Empty, Flex, message, Skeleton} from 'antd' import {Empty, Flex, message, Skeleton} from 'antd'
import type {Domain} from '../../utils/api' import {addDomainToWatchlist, Domain, Watchlist} from '../../utils/api'
import { getDomain} from '../../utils/api' import {getDomain} from '../../utils/api'
import type {AxiosError} from 'axios' import type {AxiosError} from 'axios'
import {t} from 'ttag' import {t} from 'ttag'
import type { FieldType} from '../../components/search/DomainSearchBar' import type { FieldType} from '../../components/search/DomainSearchBar'
@ -10,17 +12,18 @@ import {DomainSearchBar} from '../../components/search/DomainSearchBar'
import {DomainResult} from '../../components/search/DomainResult' import {DomainResult} from '../../components/search/DomainResult'
import {showErrorAPI} from '../../utils/functions/showErrorAPI' import {showErrorAPI} from '../../utils/functions/showErrorAPI'
import {useNavigate, useParams} from 'react-router-dom' import {useNavigate, useParams} from 'react-router-dom'
import {CaretUpOutlined, PlusOutlined} from '@ant-design/icons'
import WatchlistSelectionModal from "../../components/tracking/watchlist/WatchlistSelectionModal";
export default function DomainSearchPage() { export default function DomainSearchPage() {
const {query} = useParams() const {query} = useParams()
const [domain, setDomain] = useState<Domain | null>() const [domain, setDomain] = useState<Domain | null>()
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState(false)
const [addToWatchlistModal, setAddToWatchlistModal] = useState(false)
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate() const navigate = useNavigate()
const onFinish: FormProps<FieldType>['onFinish'] = (values) => { const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
navigate('/search/domain/' + values.ldhName) navigate('/search/domain/' + values.ldhName)
@ -41,7 +44,15 @@ export default function DomainSearchPage() {
onFinish({ldhName: query, isRefreshForced: false}) onFinish({ldhName: query, isRefreshForced: false})
}, []) }, [])
return ( const addToWatchlist = async (watchlist: Watchlist) => {
await addDomainToWatchlist(watchlist, domain!.ldhName).then(() => {
setAddToWatchlistModal(false)
}).catch((e: AxiosError) => {
showErrorAPI(e, messageApi)
})
}
return <>
<Flex gap='middle' align='center' justify='center' vertical> <Flex gap='middle' align='center' justify='center' vertical>
{contextHolder} {contextHolder}
<DomainSearchBar initialValue={query} onFinish={onFinish}/> <DomainSearchBar initialValue={query} onFinish={onFinish}/>
@ -57,5 +68,29 @@ export default function DomainSearchPage() {
} }
</Skeleton> </Skeleton>
</Flex> </Flex>
) {domain
&& <FloatButton.Group
trigger='click'
style={{
position: 'fixed',
insetInlineEnd: (100 - 40) / 2,
bottom: 100 - 40 / 2
}}
icon={<CaretUpOutlined/>}
>
<Tooltip title={t`Add to watchlist`} placement='left'>
<FloatButton icon={<PlusOutlined />} onClick={() => setAddToWatchlistModal(true)} />
</Tooltip>
</FloatButton.Group>
}
<WatchlistSelectionModal
open={addToWatchlistModal}
onFinish={addToWatchlist}
modalProps={{
title: t`Add ${domain?.ldhName} to a watchlist`,
onCancel: () => setAddToWatchlistModal(false),
onClose: () => setAddToWatchlistModal(false),
}}
/>
</>
} }

View File

@ -44,6 +44,13 @@ export async function patchWatchlist(token: string, watchlist: Partial<Watchlist
return response.data return response.data
} }
export async function addDomainToWatchlist(watchlist: Watchlist, ldhName: string) {
const domains = watchlist.domains.map(d => '/api/domains/' + d.ldhName)
domains.push('/api/domains/' + ldhName)
return patchWatchlist(watchlist.token, {domains})
}
export async function deleteWatchlist(token: string): Promise<void> { export async function deleteWatchlist(token: string): Promise<void> {
await request({ await request({
method: 'DELETE', method: 'DELETE',

View File

@ -6,10 +6,8 @@ import React from 'react'
import type {Event} from "../api" import type {Event} from "../api"
import {t} from "ttag" import {t} from "ttag"
export function DomainToTag({domain}: { domain: { ldhName: string, deleted: boolean, status: string[], events?: Event[] } }) { export function DomainToTag({domain, link}: { domain: { ldhName: string, deleted: boolean, status: string[], events?: Event[] }, link?: boolean }) {
return ( const tag = <Badge dot={domain.events?.find(e =>
<Link to={'/search/domain/' + domain.ldhName}>
<Badge dot={domain.events?.find(e =>
e.action === 'last changed' && e.action === 'last changed' &&
!e.deleted && !e.deleted &&
((new Date().getTime() - new Date(e.date).getTime()) < 7*24*60*60*1e3) ((new Date().getTime() - new Date(e.date).getTime()) < 7*24*60*60*1e3)
@ -32,6 +30,14 @@ export function DomainToTag({domain}: { domain: { ldhName: string, deleted: bool
>{punycode.toUnicode(domain.ldhName)} >{punycode.toUnicode(domain.ldhName)}
</Tag> </Tag>
</Badge> </Badge>
if (link ?? true) {
return (
<Link to={'/search/domain/' + domain.ldhName}>
{tag}
</Link> </Link>
) )
} else {
return tag
}
} }