From 8fd1b08c8d75eebf0f962de39886b1819b48c327 Mon Sep 17 00:00:00 2001 From: Xharles Date: Thu, 23 Jul 2026 10:27:54 +0100 Subject: [PATCH 1/4] set up --- .../services/exploits/exploitsEndpoints.ts | 82 ++++++++++++++ .../src/api/services/exploits/types.ts | 62 +++++++++++ packages/frontend/src/api/services/index.ts | 3 + packages/frontend/src/api/types/exploits.ts | 41 +++++++ packages/frontend/src/api/types/index.ts | 1 + .../src/components/exploits/ExploitItem.tsx | 63 +++++++++++ .../components/exploits/ExploitsModule.tsx | 98 +++++++++++++++++ packages/frontend/src/config/backend.ts | 4 + packages/frontend/src/config/widgets.ts | 6 + packages/frontend/src/constants.ts | 1 + .../containers/exploits/ExploitsContainer.tsx | 103 ++++++++++++++++++ packages/frontend/src/types.ts | 3 + 12 files changed, 467 insertions(+) create mode 100644 packages/frontend/src/api/services/exploits/exploitsEndpoints.ts create mode 100644 packages/frontend/src/api/services/exploits/types.ts create mode 100644 packages/frontend/src/api/types/exploits.ts create mode 100644 packages/frontend/src/components/exploits/ExploitItem.tsx create mode 100644 packages/frontend/src/components/exploits/ExploitsModule.tsx create mode 100644 packages/frontend/src/containers/exploits/ExploitsContainer.tsx diff --git a/packages/frontend/src/api/services/exploits/exploitsEndpoints.ts b/packages/frontend/src/api/services/exploits/exploitsEndpoints.ts new file mode 100644 index 000000000..c328acc1e --- /dev/null +++ b/packages/frontend/src/api/services/exploits/exploitsEndpoints.ts @@ -0,0 +1,82 @@ +import queryString from "query-string"; +import { Logger } from "src/api/utils/logging"; +import CONFIG from "../../../config/config"; +import { alphadayApi } from "../alphadayApi"; +import { + TGetExploitsRequest, + TGetExploitsRawResponse, + TGetExploitsResponse, +} from "./types"; + +const { EXPLOITS } = CONFIG.API.DEFAULT.ROUTES; + +const parseAmount = (amount: string | null): number | null => { + if (amount === null) return null; + const parsed = parseFloat(amount); + return Number.isNaN(parsed) ? null : parsed; +}; + +const exploitsApi = alphadayApi.injectEndpoints({ + endpoints: (builder) => ({ + getExploits: builder.query({ + query: (req) => { + const params: string = queryString.stringify({ + ...req, + limit: req.limit ?? 30, + }); + const path = `${String(EXPLOITS.BASE)}${String( + EXPLOITS.DEFAULT + )}?${params}`; + Logger.debug("getExploits: querying", path); + return path; + }, + transformResponse: ( + r: TGetExploitsRawResponse, + _meta, + _arg + ): TGetExploitsResponse => { + try { + const facadeData = r.results + .filter( + (item, index, self) => + self.findIndex( + (innerItem) => innerItem.id === item.id + ) === index + ) + .map((data) => ({ + id: data.id, + hash: data.hash, + title: data.title, + url: data.url, + image: data.image, + shortDescription: data.short_description, + description: data.description, + protocol: data.protocol, + amountUsd: parseAmount(data.amount_usd), + chain: data.chain, + attackType: data.attack_type, + date: data.date, + source: data.source, + tags: data.tags ?? [], + })); + return { + ...r, + results: facadeData, + }; + } catch (error) { + Logger.error( + "getExploits::transformResponse: error while parsing response:", + error + ); + return { + ...r, + results: [], + }; + } + }, + }), + }), + overrideExisting: false, +}); + +export const { useGetExploitsQuery } = exploitsApi; diff --git a/packages/frontend/src/api/services/exploits/types.ts b/packages/frontend/src/api/services/exploits/types.ts new file mode 100644 index 000000000..477398dea --- /dev/null +++ b/packages/frontend/src/api/services/exploits/types.ts @@ -0,0 +1,62 @@ +import { TExploit } from "src/api/types"; +import { TPagination } from "../baseTypes"; + +/** + * Primitive types + */ + +export type TRemoteExploitSource = { + name: string; + slug: string; + icon: string | null; +}; + +export type TRemoteExploitTag = { + id: number; + name: string; + slug: string; +}; + +export type TRemoteExploit = { + id: number; + hash: string; + title: string; + url: string; + image: string | null; + short_description: string | null; + description: string | null; + protocol: string | null; + // amount lost, expressed in millions of USD + amount_usd: string | null; + chain: string | null; + attack_type: string | null; + date: string; + source: TRemoteExploitSource | null; + tags: TRemoteExploitTag[]; +}; + +/** + * Queries + */ + +export type TGetExploitsRequest = { + page?: number | undefined; + limit?: number | undefined; + attack_type?: string | undefined; + chain?: string | undefined; + date?: string | undefined; + min_amount?: number | undefined; + period?: string | undefined; + protocol?: string | undefined; + sort_by?: string | undefined; + sources?: string | undefined; + tags?: string | undefined; +}; + +export type TGetExploitsRawResponse = TPagination & { + results: TRemoteExploit[]; +}; + +export type TGetExploitsResponse = TPagination & { + results: TExploit[]; +}; diff --git a/packages/frontend/src/api/services/index.ts b/packages/frontend/src/api/services/index.ts index 107128f27..2e4ae2855 100644 --- a/packages/frontend/src/api/services/index.ts +++ b/packages/frontend/src/api/services/index.ts @@ -44,6 +44,9 @@ export * from "./yields/types"; export * from "./fees/feesEndpoints"; export * from "./fees/types"; +export * from "./exploits/exploitsEndpoints"; +export * from "./exploits/types"; + export * from "./projects/projectsEndpoints"; export * from "./projects/types"; diff --git a/packages/frontend/src/api/types/exploits.ts b/packages/frontend/src/api/types/exploits.ts new file mode 100644 index 000000000..a42c21c82 --- /dev/null +++ b/packages/frontend/src/api/types/exploits.ts @@ -0,0 +1,41 @@ +export type TExploitSource = { + name: string; + slug: string; + icon: string | null; +}; + +export type TExploitTag = { + id: number; + name: string; + slug: string; +}; + +export type TExploit = { + id: number; + hash: string; + title: string; + url: string; + image: string | null; + shortDescription: string | null; + description: string | null; + protocol: string | null; + // amount lost, expressed in millions of USD (null when unknown) + amountUsd: number | null; + chain: string | null; + attackType: string | null; + date: string; + source: TExploitSource | null; + tags: TExploitTag[]; +}; + +export enum EExploitPeriod { + Last24 = "LAST_24", + LastWeek = "LAST_WEEK", + LastMonth = "LAST_MONTH", + LastQuarter = "LAST_QUARTER", +} + +export enum EExploitSortBy { + Date = "-date", + Amount = "-amount_usd", +} diff --git a/packages/frontend/src/api/types/index.ts b/packages/frontend/src/api/types/index.ts index 698e06373..e824fb6ed 100644 --- a/packages/frontend/src/api/types/index.ts +++ b/packages/frontend/src/api/types/index.ts @@ -5,6 +5,7 @@ export * from "./custom"; export * from "./dao"; export * from "./discord"; export * from "./event"; +export * from "./exploits"; export * from "./features"; export * from "./fees"; export * from "./forum"; diff --git a/packages/frontend/src/components/exploits/ExploitItem.tsx b/packages/frontend/src/components/exploits/ExploitItem.tsx new file mode 100644 index 000000000..d84671deb --- /dev/null +++ b/packages/frontend/src/components/exploits/ExploitItem.tsx @@ -0,0 +1,63 @@ +import { FC } from "react"; +import moment from "moment-with-locales-es6"; +import { TExploit } from "src/api/types"; +import { formatNumber, ENumberStyle } from "src/api/utils/format"; + +interface IExploitItemProps { + exploit: TExploit; +} + +const formatAmount = (amountUsd: number | null): string => { + if (amountUsd === null) return "—"; + // amount_usd is expressed in millions of USD + return formatNumber({ + value: amountUsd * 1_000_000, + style: ENumberStyle.Currency, + currency: "USD", + }).value; +}; + +export const ExploitItem: FC = ({ exploit }) => { + return ( + +
+
+ + {exploit.protocol ?? exploit.title} + +
+ + {exploit.title} + +
+ {exploit.chain && ( + + {exploit.chain} + + )} + {exploit.attackType && ( + + {exploit.attackType.replace(/-/g, " ")} + + )} + + {moment(exploit.date).format("MMM D, YYYY")} + +
+
+
+ + {formatAmount(exploit.amountUsd)} + + + lost + +
+
+ ); +}; diff --git a/packages/frontend/src/components/exploits/ExploitsModule.tsx b/packages/frontend/src/components/exploits/ExploitsModule.tsx new file mode 100644 index 000000000..864d4215b --- /dev/null +++ b/packages/frontend/src/components/exploits/ExploitsModule.tsx @@ -0,0 +1,98 @@ +import { FC, memo, FormEvent } from "react"; +import { ModuleLoader, ScrollBar, TabsBar } from "@alphaday/ui-kit"; +import { TExploit, EExploitSortBy } from "src/api/types"; +import { shouldFetchMoreItems } from "src/api/utils/itemUtils"; +import { Logger } from "src/api/utils/logging"; +import globalMessages from "src/globalMessages"; +import { ExploitItem } from "./ExploitItem"; + +interface IExploits { + isLoading: boolean; + exploits: TExploit[] | undefined; + widgetHeight: number; + selectedSort: EExploitSortBy; + onChangeSort: (sort: EExploitSortBy) => void; + handlePaginate: (type: "next" | "previous") => void; +} + +const SORT_HEIGHT = 38; + +const sortNavItems = [ + { label: "Latest", value: EExploitSortBy.Date }, + { label: "Biggest", value: EExploitSortBy.Amount }, +]; + +const ExploitsModule: FC = memo(function ExploitsModule({ + isLoading, + exploits, + widgetHeight, + selectedSort, + onChangeSort, + handlePaginate, +}) { + const LIST_HEIGHT = widgetHeight - SORT_HEIGHT; + + const selectedSortOption = + sortNavItems.find((item) => item.value === selectedSort) || + sortNavItems[0]; + + const onTabOptionChange = (value: string) => { + const optionItem = sortNavItems.find((item) => item.value === value); + if (optionItem === undefined) { + Logger.debug("ExploitsModule::onTabOptionChange: option not found"); + return; + } + onChangeSort(optionItem.value); + }; + + const handleListScroll = ({ currentTarget }: FormEvent) => { + if (shouldFetchMoreItems(currentTarget)) { + handlePaginate("next"); + } + }; + + if (isLoading) { + return ; + } + + return ( +
+
+
+ +
+
    + {exploits !== undefined && exploits?.length !== 0 ? ( + + {exploits?.map((exploit) => ( +
  • + +
  • + ))} +
    + ) : ( +
    +

    + {globalMessages.queries.noMatchFound( + "security alerts" + )} +

    +
    + )} +
+
+
+ ); +}); + +export default ExploitsModule; diff --git a/packages/frontend/src/config/backend.ts b/packages/frontend/src/config/backend.ts index c31015b4e..ffa663ba4 100644 --- a/packages/frontend/src/config/backend.ts +++ b/packages/frontend/src/config/backend.ts @@ -146,6 +146,10 @@ const API_V0 = { BASE: "tvl", DEFAULT: "/yields/", }, + EXPLOITS: { + BASE: "security", + DEFAULT: "/exploits/", + }, VIEWS: { BASE: "ui", AVAILABLE_VIEWS: "/views/", diff --git a/packages/frontend/src/config/widgets.ts b/packages/frontend/src/config/widgets.ts index bd8a11519..a813c7140 100644 --- a/packages/frontend/src/config/widgets.ts +++ b/packages/frontend/src/config/widgets.ts @@ -253,6 +253,12 @@ export const WIDGETS_CONFIG = { MAX_PAGE_NUMBER: 10, ADJUSTABLE: true, }, + [ETemplateNameRegistry.Exploits]: { + POLLING_INTERVAL: 15 * 60, // 15 min + WIDGET_HEIGHT: 538, + MAX_PAGE_NUMBER: 10, + ADJUSTABLE: true, + }, [ETemplateNameRegistry.TvlFees]: { TAG_ITEM_TYPE: "project", POLLING_INTERVAL: 180, // 3 min diff --git a/packages/frontend/src/constants.ts b/packages/frontend/src/constants.ts index f58e6b0bd..4137308eb 100644 --- a/packages/frontend/src/constants.ts +++ b/packages/frontend/src/constants.ts @@ -64,6 +64,7 @@ export enum ETemplateNameRegistry { Discord = "DISCORD", Dune = "DUNE", DuneTable = "DUNE_TABLE", + Exploits = "EXPLOITS", FAQ = "FAQ", Forum = "FORUM", Gas = "GAS", diff --git a/packages/frontend/src/containers/exploits/ExploitsContainer.tsx b/packages/frontend/src/containers/exploits/ExploitsContainer.tsx new file mode 100644 index 000000000..0cf3aac26 --- /dev/null +++ b/packages/frontend/src/containers/exploits/ExploitsContainer.tsx @@ -0,0 +1,103 @@ +import { FC, useState, useEffect, useRef } from "react"; +import { usePagination, useWidgetHeight } from "src/api/hooks"; +import { useGetExploitsQuery } from "src/api/services"; +import { TExploit, EExploitSortBy } from "src/api/types"; +import { buildUniqueItemList } from "src/api/utils/itemUtils"; +import ExploitsModule from "src/components/exploits/ExploitsModule"; +import CONFIG from "src/config"; +import { IModuleContainer } from "src/types"; + +const { MAX_PAGE_NUMBER } = CONFIG.WIDGETS.EXPLOITS; + +const ExploitsContainer: FC = ({ + moduleData, + mobileViewWidgetHeight, +}) => { + const widgetHeight = useWidgetHeight(moduleData); + + const [currentPage, setCurrentPage] = useState(); + const [selectedSort, setSelectedSort] = useState( + EExploitSortBy.Date + ); + + const pollingInterval = + (moduleData.widget.refresh_interval || + CONFIG.WIDGETS.EXPLOITS.POLLING_INTERVAL) * 1000; + + const { + currentData: exploitsDataResponse, + isLoading, + isSuccess, + } = useGetExploitsQuery( + { + page: currentPage, + sort_by: selectedSort, + }, + { + pollingInterval, + } + ); + const prevExploitsDataResponseRef = useRef(); + + const { + nextPage, + handleNextPage, + reset: resetPagination, + } = usePagination(exploitsDataResponse?.links, MAX_PAGE_NUMBER, isSuccess); + + const exploitsDataForCurrentPage = [ + ...(exploitsDataResponse?.results ?? []), + ]; + + const [exploitsData, setExploitsData] = useState([]); + + const reset = () => { + if (exploitsData.length !== 0) setExploitsData([]); + if (currentPage !== undefined) setCurrentPage(undefined); + resetPagination(); + }; + + // If the current response changes, it means the request parameters changed. + // This happens when the user scrolled to the bottom. + if ( + exploitsDataResponse?.results !== undefined && + prevExploitsDataResponseRef.current !== exploitsDataResponse?.results + ) { + setExploitsData((prevState) => [ + ...prevState, + ...exploitsDataForCurrentPage, + ]); + prevExploitsDataResponseRef.current = exploitsDataResponse?.results; + } + + // set current page 350ms after next page is set. + // RTK should cache requests, so we don't need to be too careful about rerenders. + useEffect(() => { + if (nextPage === undefined) return () => null; + const timeout = setTimeout(() => { + setCurrentPage(nextPage); + }, 350); + return () => { + clearTimeout(timeout); + }; + }, [nextPage]); + + const handleChangeSort = (sort: EExploitSortBy) => { + if (sort === selectedSort) return; + reset(); + setSelectedSort(sort); + }; + + return ( + + ); +}; + +export default ExploitsContainer; diff --git a/packages/frontend/src/types.ts b/packages/frontend/src/types.ts index 7a2f25c46..c827d8cf1 100644 --- a/packages/frontend/src/types.ts +++ b/packages/frontend/src/types.ts @@ -152,6 +152,9 @@ export const TEMPLATES_DICT: Partial = { tvl_fees_template: lazyRetry( () => import("./containers/fees/FeesContainer") ), + exploits_template: lazyRetry( + () => import("./containers/exploits/ExploitsContainer") + ), two_col_image_template: lazyRetry( () => import("./containers/image/TwoColImageContainer") ), From 011dc15323d7a0c34b9b05eed3d97092620bc309 Mon Sep 17 00:00:00 2001 From: Xharles Date: Thu, 23 Jul 2026 11:14:38 +0100 Subject: [PATCH 2/4] fix rendering issues --- .../services/exploits/exploitsEndpoints.ts | 25 ++++++++-- .../src/api/services/exploits/types.ts | 9 +++- .../src/components/exploits/ExploitItem.tsx | 2 +- .../containers/exploits/ExploitsContainer.tsx | 50 +++++++++++-------- 4 files changed, 60 insertions(+), 26 deletions(-) diff --git a/packages/frontend/src/api/services/exploits/exploitsEndpoints.ts b/packages/frontend/src/api/services/exploits/exploitsEndpoints.ts index c328acc1e..e8836409a 100644 --- a/packages/frontend/src/api/services/exploits/exploitsEndpoints.ts +++ b/packages/frontend/src/api/services/exploits/exploitsEndpoints.ts @@ -35,6 +35,17 @@ const exploitsApi = alphadayApi.injectEndpoints({ _meta, _arg ): TGetExploitsResponse => { + Logger.debug("[EXPLOITS] transformResponse: raw response =", r); + Logger.debug( + "[EXPLOITS] transformResponse: typeof r =", + typeof r, + "| keys =", + r && typeof r === "object" ? Object.keys(r) : "n/a", + "| results isArray =", + Array.isArray(r?.results), + "| results length =", + r?.results?.length + ); try { const facadeData = r.results .filter( @@ -54,19 +65,27 @@ const exploitsApi = alphadayApi.injectEndpoints({ protocol: data.protocol, amountUsd: parseAmount(data.amount_usd), chain: data.chain, - attackType: data.attack_type, + attackType: data.attack_type?.name ?? null, date: data.date, source: data.source, tags: data.tags ?? [], })); + Logger.debug( + "[EXPLOITS] transformResponse: mapped facade length =", + facadeData.length, + "| first item =", + facadeData[0] + ); return { ...r, results: facadeData, }; } catch (error) { Logger.error( - "getExploits::transformResponse: error while parsing response:", - error + "[EXPLOITS] transformResponse: FAILED to parse response — returning empty list. error =", + error, + "| raw response was =", + r ); return { ...r, diff --git a/packages/frontend/src/api/services/exploits/types.ts b/packages/frontend/src/api/services/exploits/types.ts index 477398dea..98daf52cb 100644 --- a/packages/frontend/src/api/services/exploits/types.ts +++ b/packages/frontend/src/api/services/exploits/types.ts @@ -17,6 +17,13 @@ export type TRemoteExploitTag = { slug: string; }; +export type TRemoteExploitAttackType = { + id: number; + name: string; + slug: string; + description: string | null; +}; + export type TRemoteExploit = { id: number; hash: string; @@ -29,7 +36,7 @@ export type TRemoteExploit = { // amount lost, expressed in millions of USD amount_usd: string | null; chain: string | null; - attack_type: string | null; + attack_type: TRemoteExploitAttackType | null; date: string; source: TRemoteExploitSource | null; tags: TRemoteExploitTag[]; diff --git a/packages/frontend/src/components/exploits/ExploitItem.tsx b/packages/frontend/src/components/exploits/ExploitItem.tsx index d84671deb..0491279ba 100644 --- a/packages/frontend/src/components/exploits/ExploitItem.tsx +++ b/packages/frontend/src/components/exploits/ExploitItem.tsx @@ -42,7 +42,7 @@ export const ExploitItem: FC = ({ exploit }) => { )} {exploit.attackType && ( - {exploit.attackType.replace(/-/g, " ")} + {exploit.attackType} )} diff --git a/packages/frontend/src/containers/exploits/ExploitsContainer.tsx b/packages/frontend/src/containers/exploits/ExploitsContainer.tsx index 0cf3aac26..1fdc2eb62 100644 --- a/packages/frontend/src/containers/exploits/ExploitsContainer.tsx +++ b/packages/frontend/src/containers/exploits/ExploitsContainer.tsx @@ -1,8 +1,9 @@ -import { FC, useState, useEffect, useRef } from "react"; +import { FC, useState, useEffect } from "react"; import { usePagination, useWidgetHeight } from "src/api/hooks"; import { useGetExploitsQuery } from "src/api/services"; import { TExploit, EExploitSortBy } from "src/api/types"; import { buildUniqueItemList } from "src/api/utils/itemUtils"; +import { Logger } from "src/api/utils/logging"; import ExploitsModule from "src/components/exploits/ExploitsModule"; import CONFIG from "src/config"; import { IModuleContainer } from "src/types"; @@ -37,7 +38,6 @@ const ExploitsContainer: FC = ({ pollingInterval, } ); - const prevExploitsDataResponseRef = useRef(); const { nextPage, @@ -45,30 +45,28 @@ const ExploitsContainer: FC = ({ reset: resetPagination, } = usePagination(exploitsDataResponse?.links, MAX_PAGE_NUMBER, isSuccess); - const exploitsDataForCurrentPage = [ - ...(exploitsDataResponse?.results ?? []), - ]; - const [exploitsData, setExploitsData] = useState([]); const reset = () => { - if (exploitsData.length !== 0) setExploitsData([]); - if (currentPage !== undefined) setCurrentPage(undefined); + setExploitsData([]); + setCurrentPage(undefined); resetPagination(); }; - // If the current response changes, it means the request parameters changed. - // This happens when the user scrolled to the bottom. - if ( - exploitsDataResponse?.results !== undefined && - prevExploitsDataResponseRef.current !== exploitsDataResponse?.results - ) { - setExploitsData((prevState) => [ - ...prevState, - ...exploitsDataForCurrentPage, - ]); - prevExploitsDataResponseRef.current = exploitsDataResponse?.results; - } + // Accumulate pages as they arrive. + // + // This runs in the commit phase, on purpose. The earlier approach appended + // during render while also mutating a ref to track "already seen" — but in + // React 18's concurrent renderer a prepared render can be discarded, which + // rolls back the queued setState while keeping the ref mutation. The ref + // then permanently reads "already appended", so the fetched page is lost + // and the list stays empty. Keying an effect on the results reference keeps + // the state update and the de-dup tracking in sync. + const exploitsResults = exploitsDataResponse?.results; + useEffect(() => { + if (exploitsResults === undefined) return; + setExploitsData((prevState) => [...prevState, ...exploitsResults]); + }, [exploitsResults]); // set current page 350ms after next page is set. // RTK should cache requests, so we don't need to be too careful about rerenders. @@ -88,9 +86,19 @@ const ExploitsContainer: FC = ({ setSelectedSort(sort); }; + const dataForModule = buildUniqueItemList(exploitsData); + Logger.debug( + "[EXPLOITS] container: passing to module. accumulated =", + exploitsData.length, + "| after dedup =", + dataForModule.length, + "| isLoading =", + isLoading + ); + return ( Date: Thu, 23 Jul 2026 14:06:45 +0100 Subject: [PATCH 3/4] improve exploit item --- .../src/components/exploits/ExploitItem.tsx | 108 ++++++++++++------ 1 file changed, 76 insertions(+), 32 deletions(-) diff --git a/packages/frontend/src/components/exploits/ExploitItem.tsx b/packages/frontend/src/components/exploits/ExploitItem.tsx index 0491279ba..c3961dfdb 100644 --- a/packages/frontend/src/components/exploits/ExploitItem.tsx +++ b/packages/frontend/src/components/exploits/ExploitItem.tsx @@ -1,7 +1,8 @@ -import { FC } from "react"; +import { FC, useState, KeyboardEvent } from "react"; import moment from "moment-with-locales-es6"; import { TExploit } from "src/api/types"; import { formatNumber, ENumberStyle } from "src/api/utils/format"; +import ChevronSVG from "src/assets/icons/chevron-down2.svg?react"; interface IExploitItemProps { exploit: TExploit; @@ -18,46 +19,89 @@ const formatAmount = (amountUsd: number | null): string => { }; export const ExploitItem: FC = ({ exploit }) => { + const [isExpanded, setIsExpanded] = useState(false); + const description = exploit.description ?? exploit.shortDescription; + + const toggle = () => setIsExpanded((prev) => !prev); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggle(); + } + }; + return ( - -
-
+ ); }; From 076ae5815bd4832e8b8f711c704d6668b6e6fef9 Mon Sep 17 00:00:00 2001 From: Xharles Date: Thu, 20 Aug 2026 12:29:12 +0100 Subject: [PATCH 4/4] fix spacing issue --- packages/frontend/src/api/services/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/frontend/src/api/services/index.ts b/packages/frontend/src/api/services/index.ts index 723250561..342c8577c 100644 --- a/packages/frontend/src/api/services/index.ts +++ b/packages/frontend/src/api/services/index.ts @@ -50,7 +50,6 @@ export * from "./exploits/types"; export * from "./developerActivity/developerActivityEndpoints"; export * from "./developerActivity/types"; - export * from "./projects/projectsEndpoints"; export * from "./projects/types";