diff --git a/src/components/AppVersion.tsx b/src/components/AppVersion.tsx index 0aa0e32..35b3081 100644 --- a/src/components/AppVersion.tsx +++ b/src/components/AppVersion.tsx @@ -30,7 +30,7 @@ const AppVersion = () => { return ( <>
@@ -40,18 +40,22 @@ const AppVersion = () => {
-
- {changelog.map((data: Record) => ( - - ))} -
- -
+
+
+
+ +
+
+
+ {changelog.map((data: Record) => ( + + ))} +
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 3f4cf1b..711d678 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,3 +1,4 @@ +import { Shortcut } from '../enums/shortcut.enum'; import AppVersion from './AppVersion'; import ShortKeys from './ShortKeys'; import ThemeController from './ThemeController'; @@ -6,8 +7,8 @@ import { FaGithub } from 'react-icons/fa'; const Footer = () => { return (
- -
+ +

© 2024 Nikola Nenovski. All right reserved.

diff --git a/src/components/HistoryModal.tsx b/src/components/HistoryModal.tsx index ba9331d..0917669 100644 --- a/src/components/HistoryModal.tsx +++ b/src/components/HistoryModal.tsx @@ -2,9 +2,12 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useHistoryModal } from '../providers/HistoryProvider'; import { HistoryObject } from '../interfaces/HistoryObject'; import { useNavigate, useSearchParams } from 'react-router-dom'; +import ShortKeys from './ShortKeys'; +import { Shortcut } from '../enums/shortcut.enum'; +import { FaTrash } from 'react-icons/fa'; const HistoryModal = () => { - const { setIsModalOpen, isModalOpen, history, historyPush } = + const { setIsModalOpen, isModalOpen, history, historyPush, clearHistory } = useHistoryModal(); const historyInputRef = useRef(null); const [searchQuery, setSearchQuery] = useState(''); @@ -56,9 +59,9 @@ const HistoryModal = () => { return ( -
+
+
@@ -115,7 +119,22 @@ const HistoryModal = () => {
+
+ +
+ + +
); }; diff --git a/src/components/Markers.tsx b/src/components/Markers.tsx index efad432..182e2de 100644 --- a/src/components/Markers.tsx +++ b/src/components/Markers.tsx @@ -1,6 +1,9 @@ import { FaCheck } from 'react-icons/fa'; import { IoClose } from 'react-icons/io5'; import { DnsRecordAnswer } from '../types/DnsRecordAnswer'; +import { useMemo } from 'react'; +import NotAvailableMarker from './NotAvailableMarker'; +import VerticalSeparator from './VerticalSeparator'; interface MarkersPropsType { AAAA: DnsRecordAnswer[] | string | undefined; @@ -8,6 +11,13 @@ interface MarkersPropsType { hasWwwRecord: boolean; dnssec: string | undefined; wwwSsl: boolean; + cdnInfo: string[]; + domainStatusCodes: string[] | string; + isWhoisLoading: boolean; + isDnsLoading: boolean; + isSslLoading: boolean; + isCdnCheckLoading: boolean; + isWpCheckLoading: boolean; } const Markers: React.FC = ({ @@ -16,45 +26,80 @@ const Markers: React.FC = ({ hasWwwRecord, dnssec, wwwSsl, + cdnInfo, + domainStatusCodes, + isWhoisLoading, + isDnsLoading, + isSslLoading, + isCdnCheckLoading, + isWpCheckLoading, }) => { + const transferCheck = useMemo(() => { + if (!domainStatusCodes) return; + + if (domainStatusCodes.includes('ok')) return 'active'; + else if (domainStatusCodes.includes('pendingTransfer')) + return 'pending transfer'; + else return 'locked'; + }, [domainStatusCodes]); + + const domainStatus = useMemo(() => { + if (!domainStatusCodes) return; + + const statuses: string[] | string = Array.isArray(domainStatusCodes) + ? domainStatusCodes.map((status: string) => + status.substring(0, status.indexOf(' ')) + ) + : domainStatusCodes.substring(0, domainStatusCodes.indexOf(' ')); + + if (statuses.includes('clientHold')) return 'hold'; + else if (statuses.includes('pendingDelete')) return 'pending delete'; + else if (statuses.includes('redemptionPeriod')) return 'redemption'; + else if (statuses.includes('inactive')) return 'inactive'; + else if (statuses.includes('pendingUpdate')) return 'pending update'; + else return 'active'; + }, [domainStatusCodes]); + return ( -
+
  • NO AAAA

    - {Array.isArray(AAAA) ? ( -
    - -
    - ) : typeof AAAA === 'string' ? ( -
    - -
    + {!AAAA ? ( + ) : ( - - N/A + + {Array.isArray(AAAA) ? ( + + ) : ( + + )} )}
  • WWW DNS

    - {hasWwwRecord ? ( -
    - -
    + {!hasWwwRecord ? ( + ) : ( -
    - -
    + + {hasWwwRecord ? : } + )}
  • -
    +
  • DNSSEC

    {!dnssec ? ( - - N/A - + ) : dnssec === 'unsigned' ? ( unsigned @@ -67,45 +112,95 @@ const Markers: React.FC = ({
  • STATUS

    - - active - +
    + {!domainStatus ? ( + + ) : ( + + {domainStatus} + + )} + + EPP Status Codes + +
  • TRANSFER

    - - locked - + {!domainStatus ? ( + + ) : ( + + {transferCheck} + + )}
  • -
    +
  • -

    HAS CDN

    -
    - -
    +

    CDN

    + {cdnInfo.length === 0 ? ( + + ) : ( +
    + {cdnInfo.length === 1 ? ( + {cdnInfo[0].toLowerCase()} + ) : ( + + multiple + + )} +
    + )}
  • WWW SSL

    - {wwwSsl ? ( -
    - -
    + {!wwwSsl ? ( + ) : ( -
    - -
    + + {wwwSsl ? : } + )}
  • HAS WP

    - {hasWp ? ( -
    - -
    + {isWpCheckLoading ? ( + ) : ( -
    - -
    + + {hasWp ? : } + )}
diff --git a/src/components/NotAvailableMarker.tsx b/src/components/NotAvailableMarker.tsx new file mode 100644 index 0000000..bbbd797 --- /dev/null +++ b/src/components/NotAvailableMarker.tsx @@ -0,0 +1,25 @@ +import { cn } from '../helpers/cn.helper'; + +const NotAvailableMarker: React.FC<{ + className?: string; + loading?: boolean; +}> = ({ className, loading }) => { + return ( +
+ {loading ? ( + + ) : ( + N/A + )} +
+ ); +}; + +export default NotAvailableMarker; diff --git a/src/components/ShortKeys.tsx b/src/components/ShortKeys.tsx index ac719d2..8a66d6f 100644 --- a/src/components/ShortKeys.tsx +++ b/src/components/ShortKeys.tsx @@ -1,23 +1,40 @@ +import React from 'react'; import { IoReturnDownBackSharp } from 'react-icons/io5'; +import { Shortcut } from '../enums/shortcut.enum'; +import { cn } from '../helpers/cn.helper'; -const ShortKeys = () => { +const ShortKeys: React.FC<{ keys: Shortcut[]; className?: string }> = ({ + keys, + className, +}) => { return ( -
-
- t{' '} - - Focus search bar -
-
- - - Enter - - - Search -
-
- Esc{' '} - - Open/close search history -
+
+ {keys.includes(Shortcut.T) && ( +
+ t{' '} + - Focus search bar +
+ )} + {keys.includes(Shortcut.ENTER) && ( +
+ + + Enter + + - Search +
+ )} + {keys.includes(Shortcut.ESCAPE) && ( +
+ Esc{' '} + - Open/close search history +
+ )}
); }; diff --git a/src/components/SslTable.tsx b/src/components/SslTable.tsx index f93e177..3169e10 100644 --- a/src/components/SslTable.tsx +++ b/src/components/SslTable.tsx @@ -1,29 +1,37 @@ import React from 'react'; -import { H1 } from '../hoc/H1'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const SslTable: React.FC<{ content: Record }> = ({ content }) => { +const SslTable: React.FC<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + content: Record | undefined; + loading: boolean; +}> = ({ content, loading }) => { return ( -
- {!content ? ( -

No SSL certificate found.

+
+ {loading ? ( +
+ +
+ ) : !content ? ( +
+

No SSL certificate found.

+
) : ( - + - + - + - + diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 915eb94..28c8b1a 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -8,7 +8,7 @@ const Table: React.FC<{ }> = ({ content, type }) => { return ( typeof content !== 'string' && ( -
+

{type.toUpperCase()}

IssuerIssuer {content.issuer?.friendly_name}
CoversCovers {content.dns_names?.join(', ')}
Issued OnIssued On {new Date(content.not_before).toLocaleDateString()}
Expires OnExpires On {new Date(content.not_after).toLocaleDateString()}
diff --git a/src/components/ThemeController.tsx b/src/components/ThemeController.tsx index 6ed18ae..3c29b48 100644 --- a/src/components/ThemeController.tsx +++ b/src/components/ThemeController.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { Theme } from '../enums/Theme.enum'; import { useTheme } from '../providers/ThemeProvider'; import { FaPaintRoller } from 'react-icons/fa'; +import { MdKeyboardArrowUp } from 'react-icons/md'; const ThemeController: React.FC = () => { const { theme, changeTheme } = useTheme(); @@ -16,19 +17,11 @@ const ThemeController: React.FC = () => { > {theme} - - - +
    {themes.map((theme: Theme, idx: number) => (
  • diff --git a/src/components/VerticalSeparator.tsx b/src/components/VerticalSeparator.tsx new file mode 100644 index 0000000..231b6ce --- /dev/null +++ b/src/components/VerticalSeparator.tsx @@ -0,0 +1,5 @@ +const VerticalSeparator = () => { + return
    ; +}; + +export default VerticalSeparator; diff --git a/src/components/WhoisTable.tsx b/src/components/WhoisTable.tsx index 6109650..183537a 100644 --- a/src/components/WhoisTable.tsx +++ b/src/components/WhoisTable.tsx @@ -1,29 +1,32 @@ import React from 'react'; -import { H1 } from '../hoc/H1'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const WhoisTable: React.FC<{ content: Record | string }> = ({ - content, -}) => { +import { getDays } from '../helpers/getDays'; +const WhoisTable: React.FC<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + content: Record | undefined; + loading: boolean; +}> = ({ content, loading }) => { return ( -
    - {typeof content === 'string' ? ( -

    - Domain name is not registered or there was a problem fetching the - data. -

    +
    + {loading ? ( +
    + +
    + ) : !content ? ( +
    +

    Data could not be fetched.

    +
    ) : (
- - + + - - + + - + - + - + - - + +
Registrar{content.result!.registrar}Registrar{content?.result!.registrar}
Name Servers
Name Servers
    - {content.result!.name_servers.map( + {content?.result!.name_servers.map( (ns: string, idx: number) => (
  • {ns}
  • ) @@ -32,31 +35,39 @@ const WhoisTable: React.FC<{ content: Record | string }> = ({
Registered OnRegistered On - {new Date(content.result!.creation_date).toLocaleDateString()} + {new Date(content?.result!.creation_date).toLocaleDateString()}
Expires OnExpires On - {new Date(content.result!.expiration_date).toLocaleDateString()} + {new Date( + content?.result!.expiration_date + ).toLocaleDateString()}{' '} + ({getDays(content?.result!.expiration_date)} days)
Last updated OnLast updated On - {new Date(content.result!.updated_date).toLocaleDateString()} + {new Date(content?.result!.updated_date).toLocaleDateString()}
Status - {typeof content.result!.status === 'string' ? ( - content.result!.status + {typeof content?.result!.status === 'string' ? ( +

+ {content?.result!.status.substring( + 0, + content?.result!.status.indexOf(' ') + )} +

) : (
    - {content.result!.status.map((row: string, idx: number) => ( + {content?.result!.status.map((row: string, idx: number) => (
  • {row.substring(0, row.indexOf(' '))}
  • ))}
@@ -64,8 +75,8 @@ const WhoisTable: React.FC<{ content: Record | string }> = ({
DNSSEC{content.result!.dnssec}DNSSEC{content?.result!.dnssec}
diff --git a/src/enums/Shortcut.enum.ts b/src/enums/Shortcut.enum.ts new file mode 100644 index 0000000..74469bd --- /dev/null +++ b/src/enums/Shortcut.enum.ts @@ -0,0 +1,5 @@ +export enum Shortcut { + T = 't', + ENTER = 'enter', + ESCAPE = 'escape', +} diff --git a/src/enums/Theme.enum.ts b/src/enums/Theme.enum.ts index 2d0932b..f91eb90 100644 --- a/src/enums/Theme.enum.ts +++ b/src/enums/Theme.enum.ts @@ -3,4 +3,5 @@ export enum Theme { SOLARIZED_LIGHT = 'solarizedLight', SERIKA_DARK = 'serikaDark', SERIKA_LIGHT = 'serikaLight', + DEFAULT_DARK = 'dark', } diff --git a/src/helpers/getDays.ts b/src/helpers/getDays.ts new file mode 100644 index 0000000..2f8f7c4 --- /dev/null +++ b/src/helpers/getDays.ts @@ -0,0 +1,5 @@ +export const getDays = (ms: number): number => { + return Math.floor( + (new Date(ms).getTime() - Date.now()) / 1000 / 60 / 60 / 24 + ); +}; diff --git a/src/hoc/ViewContainer.tsx b/src/hoc/ViewContainer.tsx index 1be0450..d9f97b0 100644 --- a/src/hoc/ViewContainer.tsx +++ b/src/hoc/ViewContainer.tsx @@ -1,8 +1,17 @@ import React, { ReactNode } from 'react'; +import { cn } from '../helpers/cn.helper'; -const ViewContainer: React.FC<{ children: ReactNode }> = ({ children }) => { +const ViewContainer: React.FC<{ children: ReactNode; className?: string }> = ({ + children, + className, +}) => { return ( -
+
{children}
); diff --git a/src/providers/HistoryProvider.tsx b/src/providers/HistoryProvider.tsx index ca59559..3015821 100644 --- a/src/providers/HistoryProvider.tsx +++ b/src/providers/HistoryProvider.tsx @@ -11,6 +11,7 @@ interface HistoryContextType { // eslint-disable-next-line @typescript-eslint/no-explicit-any history: any; historyPush: (newDomain: string) => void; + clearHistory: () => void; handleOpenCloseModal: (event: KeyboardEvent) => void; setIsModalOpen: React.Dispatch>; } @@ -39,6 +40,10 @@ export const HistoryProvider: React.FC<{ children: ReactNode }> = ({ setHistory([...history, { domain: newDomain, searchedOn: Date.now() }]); }; + const clearHistory = () => { + setHistory([]); + }; + const handleOpenCloseModal = (event: KeyboardEvent) => { if (event.key === 'Escape') setIsModalOpen(prev => !prev); }; @@ -51,6 +56,7 @@ export const HistoryProvider: React.FC<{ children: ReactNode }> = ({ isModalOpen, handleOpenCloseModal, setIsModalOpen, + clearHistory, }} > {children} diff --git a/src/services/cdnCheck.service.ts b/src/services/cdnCheck.service.ts new file mode 100644 index 0000000..cb1d140 --- /dev/null +++ b/src/services/cdnCheck.service.ts @@ -0,0 +1,18 @@ +import axios from 'axios'; + +export const isCdnActive = async (domain: string) => { + try { + const response = await axios.get( + `https://api-dl.nikola-nenovski.info/api/v1/cdn-check?domain=${domain}`, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + return response.data.data; + } catch (error) { + console.error(error); + } +}; diff --git a/src/services/http/whois.get.endpoints.http b/src/services/http/whois.get.endpoints.http index e43fdfe..f6fb4e8 100644 --- a/src/services/http/whois.get.endpoints.http +++ b/src/services/http/whois.get.endpoints.http @@ -12,3 +12,11 @@ GET {{WHOIS_API_URL}} ?domain={{MISSPELLED_DOMAIN}} apikey:{{WHOIS_API_KEY}} // WhoIS request with a misspelled domain name + + +### +# @import ./variables.http + +GET {{WHOIS_API_URL}} + ?domain=nikolanenovski.com + apikey:{{WHOIS_API_KEY}} \ No newline at end of file diff --git a/src/services/wpCheck.service.ts b/src/services/wpCheck.service.ts index 3f63004..08c2ef9 100644 --- a/src/services/wpCheck.service.ts +++ b/src/services/wpCheck.service.ts @@ -3,7 +3,7 @@ import axios from 'axios'; export const isWordpressInstalled = async (domain: string) => { try { const response = await axios.get( - `https://domainlookup.nicknenovski.workers.dev/wp-check?domain=${domain}`, + `https://api-dl.nikola-nenovski.info/api/v1/wp-check?domain=${domain}`, { headers: { 'Content-Type': 'application/json', @@ -11,7 +11,7 @@ export const isWordpressInstalled = async (domain: string) => { } ); - return response.data; + return response.data.data; } catch (error) { console.error(error); } diff --git a/src/views/Results.tsx b/src/views/Results.tsx index b0cf656..8ff7d64 100644 --- a/src/views/Results.tsx +++ b/src/views/Results.tsx @@ -7,7 +7,6 @@ import { useSearchParams } from 'react-router-dom'; import { getDomainSslInfo } from '../services/ssl.service'; import { getDomainInfo } from '../services/whois.service'; import { getDnsRecordInfo } from '../services/dns.service'; -import ProgressBar from '../components/ProgressBar'; import Table from '../components/Table'; import React from 'react'; import { DnsRecordAnswer } from '../types/DnsRecordAnswer'; @@ -17,6 +16,7 @@ import WhoisTable from '../components/WhoisTable'; import { H1 } from '../hoc/H1'; import Markers from '../components/Markers'; import { isWordpressInstalled } from '../services/wpCheck.service'; +import { isCdnActive } from '../services/cdnCheck.service'; interface WhoIsData { result?: Record; @@ -25,16 +25,13 @@ interface WhoIsData { const Results = () => { const [searchParams] = useSearchParams(); - const [sslData, setSslData] = useState>(); - const [whoIsData, setWhoIsData] = useState(); const [dnsData, setDnsData] = useState>(); const [hasWp, setHasWp] = useState(false); + const [cdnInfo, setCdnInfo] = useState([]); const [hasWwwRecord, setHasWwwRecord] = useState(false); - const [progress, setProgress] = useState(0); - const [isLoading, setIsLoading] = useState(false); const wwwSsl = useMemo(() => { const domain = searchParams.get('domain'); @@ -45,93 +42,110 @@ const Results = () => { ); }, [sslData, searchParams]); - useEffect(() => { - setProgress(0); - setIsLoading(true); + const [isWhoIsLoading, setIsWhoisLoading] = useState(false); + const [isDnsLoading, setIsDnsLoading] = useState(false); + const [isSslLoading, setIsSslLoading] = useState(false); + const [isWpCheckLoading, setIsWpCheckLoading] = useState(false); + const [isCdnCheckLoading, setIsCdnCheckLoading] = useState(false); + useEffect(() => { const domain = searchParams.get('domain'); if (!domain) return; - const fetchData = async () => { - try { + setIsWhoisLoading(true); + setIsSslLoading(true); + setIsDnsLoading(true); + setIsWpCheckLoading(true); + setIsCdnCheckLoading(true); + + try { + const fetchData = async () => { const tasks = [ getDomainSslInfo(domain).then(data => { setSslData(data[data.length - 1]); - setProgress(prevValue => prevValue + 1); + setIsSslLoading(false); }), getDomainInfo(domain).then(data => { setWhoIsData(data); - setProgress(prevValue => prevValue + 1); + setIsWhoisLoading(false); }), getDnsRecordInfo(domain).then(data => { setDnsData(data); - setProgress(prevValue => prevValue + 1); + setIsDnsLoading(false); }), getDnsRecordInfo(`www.${domain}`).then(data => { const answer = Array.isArray(data.A) || Array.isArray(data.CNAME); setHasWwwRecord(answer); - setProgress(prevValue => prevValue + 1); }), isWordpressInstalled(domain).then(data => { - setHasWp(data.isInstalled); - setProgress(prevValue => prevValue + 1); + setHasWp(data.wordpressInstalled); + setIsWpCheckLoading(false); + }), + isCdnActive(domain).then(data => { + setCdnInfo(Object.keys(data)); + setIsCdnCheckLoading(false); }), ]; await Promise.all(tasks); - } catch (error) { - console.error(error); - } finally { - setIsLoading(false); - } - }; + }; - fetchData(); + fetchData(); + } catch (error) { + console.error(error); + } }, [searchParams]); return ( <> - - {isLoading ? ( - - ) : ( - <> -

- Looking up {searchParams.get('domain')} -

- -
-

DNS Info

-
- {dnsData && - Object.entries(dnsData).map(([type, answer]) => { - return ( - - - - ); - })} - - -
-
-

WHOIS Info

- {whoIsData && } -
-
-

SSL Info

- {sslData && } + +

+ Looking up {searchParams.get('domain')} +

+ +
+

DNS Info

+
+ {isDnsLoading ? ( +
+
-
- - )} + ) : ( + dnsData && + Object.entries(dnsData).map(([type, answer]) => { + return ( + +
+ + ); + }) + )} + + +
+
+

WHOIS Info

+ +
+
+

SSL Info

+ +
+
diff --git a/tailwind.config.js b/tailwind.config.js index 24ce6ef..d50b881 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -8,6 +8,7 @@ export default { }, daisyui: { themes: [ + "dark", { serikaDark: { primary: '#e2b714',