mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-17 17:55:42 +00:00
feat: add domain to watchlist FAB
This commit is contained in:
parent
c3832f06c3
commit
841e8dcba6
@ -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>
|
||||||
|
}
|
||||||
@ -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),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user