From ff1229a52881a66e5bccef141287ad045c61048b Mon Sep 17 00:00:00 2001 From: nikitita Date: Tue, 20 Oct 2026 15:54:09 +0200 Subject: [PATCH 1/2] refactor: improve frontend component structure and threat actor hooks --- .../FeedBack}/FeedbackCard.css | 0 .../FeedBack}/FeedbackCard.tsx | 0 .../{layout => common/Layout}/AppLayout.css | 0 .../{layout => common/Layout}/AppLayout.tsx | 0 .../Layout}/header/Header.css | 0 .../Layout}/header/Header.tsx | 0 .../Layout}/sidebar/SideBar.css | 0 .../Layout}/sidebar/SideBar.tsx | 0 .../Navigation}/BackButton.css | 0 .../Navigation}/BackButton.tsx | 0 .../common/PageHeader/PageHeader.css | 31 ++ .../common/PageHeader/PageHeader.tsx | 39 +++ .../components/common/SearchBar/SearchBar.css | 95 +++++++ .../components/common/SearchBar/SearchBar.tsx | 52 ++++ .../TacticDetailedHeader.tsx | 5 +- .../TacticToolbar/TacticToolbar.tsx | 18 +- .../TechniqueToolbar/TechniqueToolbar.tsx | 17 +- .../TechniqueDetailedHeader.tsx | 6 +- .../ThreatActorPageHeader.css | 21 -- .../ThreatActorPageHeader.tsx | 15 - .../ThreatActorSearch/ThreatActorSearch.css | 67 ----- .../ThreatActorSearch/ThreatActorSearch.tsx | 37 --- .../ThreatActorCard/ThreatActorCard.css | 0 .../ThreatActorCard/ThreatActorCard.tsx | 0 .../ThreatActorActiveFilters.css | 0 .../ThreatActorActiveFilters.tsx | 0 .../ThreatActorFilter/ThreatActorFilter.css | 0 .../ThreatActorFilter/ThreatActorFilter.tsx | 21 +- .../ThreatActorFilterDropdown.css | 0 .../ThreatActorFilterDropdown.tsx | 0 .../ThreatActorGrid/ThreatActorGrid.css | 0 .../ThreatActorGrid/ThreatActorGrid.tsx | 0 .../ThreatActorAssociatedList.css | 0 .../ThreatActorAssociatedList.tsx | 2 +- .../ThreatActorDetailedDescription.css | 0 .../ThreatActorDetailedDescription.tsx | 0 .../ThreatActorDetailedHeader.css | 0 .../ThreatActorDetailedHeader.tsx | 3 +- .../ThreatActorDetailedInfoCard.css | 0 .../ThreatActorDetailedInfoCard.tsx | 0 .../ThreatActorDetailedInfoCards.css | 0 .../ThreatActorDetailedInfoCards.tsx | 0 .../ThreatActorOverview.css | 0 .../ThreatActorOverview.tsx | 0 .../threatactor/useThreatActorCatalog.ts | 106 +++++++ .../useThreatActorCatalogQueries.ts | 65 +++++ .../hooks/threatactor/useThreatActorDetail.ts | 20 ++ .../useThreatActorDetailQueries.ts | 95 +++++++ frontend/src/pages/Home/Home.css | 2 +- frontend/src/pages/Home/Home.tsx | 15 +- frontend/src/pages/SavedItems/SavedItems.tsx | 13 +- frontend/src/pages/Software/Software.css | 1 - frontend/src/pages/Software/Software.tsx | 14 +- .../src/pages/Software/SoftwareDetail.tsx | 2 +- .../ThreatActorDetailed.css | 0 .../ThreatActorDetailed.tsx | 87 +++--- .../ThreatActors.css | 0 .../src/pages/ThreatActor/ThreatActors.tsx | 121 ++++++++ .../src/pages/ThreatActors/ThreatActors.tsx | 172 ----------- frontend/src/routes/AppRoutes.tsx | 21 +- frontend/src/types/ThreatActorTypes.ts | 269 ++++++++++++------ 61 files changed, 907 insertions(+), 525 deletions(-) rename frontend/src/components/{feedback => common/FeedBack}/FeedbackCard.css (100%) rename frontend/src/components/{feedback => common/FeedBack}/FeedbackCard.tsx (100%) rename frontend/src/components/{layout => common/Layout}/AppLayout.css (100%) rename frontend/src/components/{layout => common/Layout}/AppLayout.tsx (100%) rename frontend/src/components/{layout => common/Layout}/header/Header.css (100%) rename frontend/src/components/{layout => common/Layout}/header/Header.tsx (100%) rename frontend/src/components/{layout => common/Layout}/sidebar/SideBar.css (100%) rename frontend/src/components/{layout => common/Layout}/sidebar/SideBar.tsx (100%) rename frontend/src/components/{navigation => common/Navigation}/BackButton.css (100%) rename frontend/src/components/{navigation => common/Navigation}/BackButton.tsx (100%) create mode 100644 frontend/src/components/common/PageHeader/PageHeader.css create mode 100644 frontend/src/components/common/PageHeader/PageHeader.tsx create mode 100644 frontend/src/components/common/SearchBar/SearchBar.css create mode 100644 frontend/src/components/common/SearchBar/SearchBar.tsx delete mode 100644 frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.css delete mode 100644 frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.tsx delete mode 100644 frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.css delete mode 100644 frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.tsx rename frontend/src/components/threatactor/{ThreatActorMain => catalog}/ThreatActorCard/ThreatActorCard.css (100%) rename frontend/src/components/threatactor/{ThreatActorMain => catalog}/ThreatActorCard/ThreatActorCard.tsx (100%) rename frontend/src/components/threatactor/{ThreatActorMain => catalog}/ThreatActorFilter/ThreatActorActiveFilters.css (100%) rename frontend/src/components/threatactor/{ThreatActorMain => catalog}/ThreatActorFilter/ThreatActorActiveFilters.tsx (100%) rename frontend/src/components/threatactor/{ThreatActorMain => catalog}/ThreatActorFilter/ThreatActorFilter.css (100%) rename frontend/src/components/threatactor/{ThreatActorMain => catalog}/ThreatActorFilter/ThreatActorFilter.tsx (91%) rename frontend/src/components/threatactor/{ThreatActorMain => catalog}/ThreatActorFilter/ThreatActorFilterDropdown.css (100%) rename frontend/src/components/threatactor/{ThreatActorMain => catalog}/ThreatActorFilter/ThreatActorFilterDropdown.tsx (100%) rename frontend/src/components/threatactor/{ThreatActorMain => catalog}/ThreatActorGrid/ThreatActorGrid.css (100%) rename frontend/src/components/threatactor/{ThreatActorMain => catalog}/ThreatActorGrid/ThreatActorGrid.tsx (100%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorAssociated/ThreatActorAssociatedList.css (100%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorAssociated/ThreatActorAssociatedList.tsx (97%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css (100%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx (100%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css (100%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx (93%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css (100%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx (100%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css (100%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx (100%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorDetailedOverview/ThreatActorOverview.css (100%) rename frontend/src/components/threatactor/{ThreatActorDetailed => detailed}/ThreatActorDetailedOverview/ThreatActorOverview.tsx (100%) create mode 100644 frontend/src/hooks/threatactor/useThreatActorCatalog.ts create mode 100644 frontend/src/hooks/threatactor/useThreatActorCatalogQueries.ts create mode 100644 frontend/src/hooks/threatactor/useThreatActorDetail.ts create mode 100644 frontend/src/hooks/threatactor/useThreatActorDetailQueries.ts rename frontend/src/pages/{ThreatActors => ThreatActor}/ThreatActorDetailed.css (100%) rename frontend/src/pages/{ThreatActors => ThreatActor}/ThreatActorDetailed.tsx (65%) rename frontend/src/pages/{ThreatActors => ThreatActor}/ThreatActors.css (100%) create mode 100644 frontend/src/pages/ThreatActor/ThreatActors.tsx delete mode 100644 frontend/src/pages/ThreatActors/ThreatActors.tsx diff --git a/frontend/src/components/feedback/FeedbackCard.css b/frontend/src/components/common/FeedBack/FeedbackCard.css similarity index 100% rename from frontend/src/components/feedback/FeedbackCard.css rename to frontend/src/components/common/FeedBack/FeedbackCard.css diff --git a/frontend/src/components/feedback/FeedbackCard.tsx b/frontend/src/components/common/FeedBack/FeedbackCard.tsx similarity index 100% rename from frontend/src/components/feedback/FeedbackCard.tsx rename to frontend/src/components/common/FeedBack/FeedbackCard.tsx diff --git a/frontend/src/components/layout/AppLayout.css b/frontend/src/components/common/Layout/AppLayout.css similarity index 100% rename from frontend/src/components/layout/AppLayout.css rename to frontend/src/components/common/Layout/AppLayout.css diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/common/Layout/AppLayout.tsx similarity index 100% rename from frontend/src/components/layout/AppLayout.tsx rename to frontend/src/components/common/Layout/AppLayout.tsx diff --git a/frontend/src/components/layout/header/Header.css b/frontend/src/components/common/Layout/header/Header.css similarity index 100% rename from frontend/src/components/layout/header/Header.css rename to frontend/src/components/common/Layout/header/Header.css diff --git a/frontend/src/components/layout/header/Header.tsx b/frontend/src/components/common/Layout/header/Header.tsx similarity index 100% rename from frontend/src/components/layout/header/Header.tsx rename to frontend/src/components/common/Layout/header/Header.tsx diff --git a/frontend/src/components/layout/sidebar/SideBar.css b/frontend/src/components/common/Layout/sidebar/SideBar.css similarity index 100% rename from frontend/src/components/layout/sidebar/SideBar.css rename to frontend/src/components/common/Layout/sidebar/SideBar.css diff --git a/frontend/src/components/layout/sidebar/SideBar.tsx b/frontend/src/components/common/Layout/sidebar/SideBar.tsx similarity index 100% rename from frontend/src/components/layout/sidebar/SideBar.tsx rename to frontend/src/components/common/Layout/sidebar/SideBar.tsx diff --git a/frontend/src/components/navigation/BackButton.css b/frontend/src/components/common/Navigation/BackButton.css similarity index 100% rename from frontend/src/components/navigation/BackButton.css rename to frontend/src/components/common/Navigation/BackButton.css diff --git a/frontend/src/components/navigation/BackButton.tsx b/frontend/src/components/common/Navigation/BackButton.tsx similarity index 100% rename from frontend/src/components/navigation/BackButton.tsx rename to frontend/src/components/common/Navigation/BackButton.tsx diff --git a/frontend/src/components/common/PageHeader/PageHeader.css b/frontend/src/components/common/PageHeader/PageHeader.css new file mode 100644 index 0000000..2ddbc22 --- /dev/null +++ b/frontend/src/components/common/PageHeader/PageHeader.css @@ -0,0 +1,31 @@ + +/* ===== Felles Page Header ===== */ + +.page-header { + display: flex; + flex-direction: column; + gap: 8px; +} + +.page-header__eyebrow { + color: var(--text-muted); + + font-size: 13px; + font-weight: 700; + letter-spacing: 0.08em; +} + +.page-header__title { + font-size: clamp(28px, 3vw, 40px); + font-weight: 800; + line-height: 1; +} + +.page-header__description { + max-width: 760px; + + color: var(--text); + + font-size: 16px; + line-height: 1.5; +} diff --git a/frontend/src/components/common/PageHeader/PageHeader.tsx b/frontend/src/components/common/PageHeader/PageHeader.tsx new file mode 100644 index 0000000..b6b7c63 --- /dev/null +++ b/frontend/src/components/common/PageHeader/PageHeader.tsx @@ -0,0 +1,39 @@ +/** Én felles css for header design */ +import "./PageHeader.css"; + +type PageHeaderProps = { + title: string; + description: string; + children?: React.ReactNode; // brukes i software +}; + +/** + * Gjenbrukbar header-komponent for applikasjonens sider. + * + * Viser en felles overskrift (eyebrow), sidetittel og beskrivelse. + * Bruker samme CSS på tvers av sidene for å sikre konsistent design. + * + * @param title - Sidens hovedoverskrift. + * @param description - Kort beskrivelse av sidens innhold. + * @param children - Valgfritt React-innhold som vises under beskrivelsen. + */ +function PageHeader({title, description}: PageHeaderProps) { + const eyebrow = "THREAT INTELLIGENCE" + return ( +
+ {eyebrow && ( + + {eyebrow} + + )} + +

{title}

+ +

+ {description} +

+
+ ); +} + +export default PageHeader; \ No newline at end of file diff --git a/frontend/src/components/common/SearchBar/SearchBar.css b/frontend/src/components/common/SearchBar/SearchBar.css new file mode 100644 index 0000000..7709511 --- /dev/null +++ b/frontend/src/components/common/SearchBar/SearchBar.css @@ -0,0 +1,95 @@ + +/* ===== Felles SearchBar ===== */ + +.search-bar { + width: 100%; + min-height: 52px; + + display: flex; + align-items: center; + + overflow: hidden; + + background: var(--surface); + + border: 1px solid var(--border); + border-radius: 8px; + + transition: + border-color 0.2s ease, + background-color 0.2s ease; +} + +/* Marker søkefeltet når input har fokus */ +.search-bar:focus-within { + border-color: var(--accent); +} + +/* Container for søkeikon og input */ +.search-bar__field { + flex: 1; + min-width: 0; + + display: flex; + align-items: center; +} + +/* Søkeikon */ +.search-bar__field > svg { + width: 22px; + height: 22px; + + margin-left: 16px; + + color: var(--text-muted); + flex-shrink: 0; +} + +.search-bar:focus-within .search-bar__field > svg { + color: var(--accent); +} + +/* Inputfelt */ +.search-bar__field input { + flex: 1; + min-width: 0; + + padding: 14px 16px; + + border: none; + outline: none; + + background: transparent; + color: #f0f7f7; + font-size: 14px; +} + +/* Search-knapp */ +.search-bar__button { + align-self: stretch; + + min-width: 120px; + padding: 0 24px; + + border: none; + + background: var(--accent); + color: var(--bg); + + font-weight: 700; + cursor: pointer; +} + +/* Tastaturnavigasjon */ +.search-bar__button:focus-visible { + outline: 2px solid var(--text); + outline-offset: -4px; +} + +/* Tilpasning for mindre skjermer */ +@media (max-width: 480px) { + .search-bar__button { + min-width: 90px; + padding: 0 12px; + } +} diff --git a/frontend/src/components/common/SearchBar/SearchBar.tsx b/frontend/src/components/common/SearchBar/SearchBar.tsx new file mode 100644 index 0000000..f6fb5ba --- /dev/null +++ b/frontend/src/components/common/SearchBar/SearchBar.tsx @@ -0,0 +1,52 @@ + +import { Search } from "lucide-react"; +import "./SearchBar.css"; + +type SearchBarProps = { + value: string; + placeholder: string; + onChange: (value: string) => void; + onSearch?: (value: string) => void; +}; + +/** + * Felles søkefelt for katalogsidene. + * + * Komponenten håndterer kun presentasjon og brukerinteraksjon. + * Selve søkelogikken bestemmes av komponenten som bruker SearchBar. + * + * @param value - Nåværende tekst i søkefeltet. + * @param placeholder - Hjelpetekst i inputfeltet. + * @param onChange - Kalles når søketeksten endres. + * @param onSearch - Valgfri callback når søket sendes inn. + */ +function SearchBar({value, placeholder,onChange, onSearch}: SearchBarProps) { + return ( +
{ + event.preventDefault(); + onSearch?.(value); + }} + > +
+
+ + +
+ ); +} + +export default SearchBar; diff --git a/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx b/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx index 3f0335d..3e92050 100644 --- a/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx +++ b/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx @@ -1,8 +1,7 @@ -import { ArrowLeft, Star } from "lucide-react"; -import { Link } from "react-router-dom"; +import { Star } from "lucide-react"; import "./TacticDetailedHeader.css"; -import BackButton from "../../../navigation/BackButton"; +import BackButton from "../../../common/Navigation/BackButton"; type TacticDetailedHeaderProps = { mitreId: string; diff --git a/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx b/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx index 8bbe305..92ab8b2 100644 --- a/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx +++ b/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx @@ -1,22 +1,14 @@ import { Search, ArrowUpDown, ChevronDown } from "lucide-react"; import "./TacticToolbar.css"; - +import PageHeader from "../../../common/PageHeader/PageHeader"; function TacticToolbar() { return (
-
- - THREAT INTELLIGENCE - - -

Tactics

- -

- Explore ATT&CK tactics and adversary objectives to understand - the goals behind cyber attacks. -

-
+
diff --git a/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx b/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx index d1b17ff..8fa154c 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx @@ -5,22 +5,15 @@ import { } from "lucide-react"; import "./TechniqueToolbar.css"; +import PageHeader from "../../../common/PageHeader/PageHeader"; function TechniqueToolbar() { return (
-
- - THREAT INTELLIGENCE - - -

Techniques

- -

- Browse documented techniques and search by name, - MITRE ID, or keyword. -

-
+
diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx b/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx index 65fbb41..2a6ea93 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx +++ b/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx @@ -1,8 +1,8 @@ -import { ArrowLeft, Star } from "lucide-react"; -import { Link } from "react-router-dom"; +import { Star } from "lucide-react"; + import "./TechniqueDetailedHeader.css"; -import BackButton from "../../../navigation/BackButton"; +import BackButton from "../../../common/Navigation/BackButton"; type TechniqueDetailedHeaderProps = { mitreId: string; diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.css b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.css deleted file mode 100644 index 09ac9ab..0000000 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.css +++ /dev/null @@ -1,21 +0,0 @@ -.threat-actor-page-header { - display: flex; - flex-direction: column; - gap: 6px; - - margin-bottom: 4px; -} - -.threat-actor-page-header h1 { - font-size: clamp(28px, 3vw, 38px); - line-height: 1.1; -} - -.page-description { - max-width: 720px; - - color: var(--text-muted); - - font-size: 16px; - line-height: 1.5; -} \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.tsx b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.tsx deleted file mode 100644 index 8ebe5e7..0000000 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader.tsx +++ /dev/null @@ -1,15 +0,0 @@ - - -function ThreatActorPageHeader(){ - return ( -
-

Threat Actors

- -

- Browse documented threat groups and search by name, alias, MITRE ID, or keyword. -

-
- ); -} - -export default ThreatActorPageHeader; \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.css b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.css deleted file mode 100644 index 466d447..0000000 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.css +++ /dev/null @@ -1,67 +0,0 @@ -.threat-actor-search { - width: 100%; - min-height: 52px; - - display: flex; - align-items: center; - - overflow: hidden; - - background: var(--surface); - - border: 1px solid var(--border); - border-radius: 8px; - - transition: - border-color 0.2s ease, - background-color 0.2s ease; -} - -.threat-actor-search:focus-within { - border-color: var(--accent); -} - -.threat-actor-search > svg { - width: 22px; - height: 22px; - - margin-left: 16px; - - color: var(--text-muted); - flex-shrink: 0; -} - -.threat-actor-search:focus-within > svg { - color: var(--accent); -} - -.threat-actor-search input { - flex: 1; - min-width: 0; - - padding: 14px 16px; - - border: none; - outline: none; - - background: transparent; - color: #f0f7f7; - font-size: 14px; - -} - -.threat-actor-search button { - align-self: stretch; - - min-width: 120px; - padding: 0 24px; - - border: none; - - background: var(--accent); - color: var(--bg); - - font-weight: 700; - - cursor: pointer; -} \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.tsx b/frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.tsx deleted file mode 100644 index a9a05c5..0000000 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Search } from "lucide-react"; -import "./ThreatActorSearch.css"; -import {type SubmitEvent } from "react"; - -type ThreatActorSearchProps = { - value: string; - onChange: (value: string) => void; - onSearch: (searchWord: string) => void; -}; - -function ThreatActorSearch({ value, onChange, onSearch}: ThreatActorSearchProps) { - - const handleSearch = (event: SubmitEvent) => { - event.preventDefault(); - onSearch(value.trim()); - }; - - return ( -
- - - onChange(event.target.value)} - /> - - - - ); -} - -export default ThreatActorSearch \ No newline at end of file diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.css b/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.css similarity index 100% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.css rename to frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.css diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.tsx b/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.tsx similarity index 100% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorCard/ThreatActorCard.tsx rename to frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.tsx diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters.css b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.css similarity index 100% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters.css rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.css diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters.tsx b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.tsx similarity index 100% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters.tsx rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.tsx diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.css b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.css similarity index 100% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.css rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.css diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.tsx similarity index 91% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.tsx index 1807a5d..f4292b2 100644 --- a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter.tsx +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.tsx @@ -12,7 +12,7 @@ import { useState } from "react"; import FilterDropdown from "./ThreatActorFilterDropdown"; import type { FilterOption } from "./ThreatActorFilterDropdown"; import "./ThreatActorFilter.css"; -import type { ThreatActorFilterOptionsData } from "../../../../types/ThreatActorTypes"; +import { DEFAULT_FILTERS, type ThreatActorFilterOptionsData, type ThreatActorFilterValues } from "../../../../types/ThreatActorTypes"; const sortOptions: FilterOption[] = [ { value: "name-asc", label: "Name (A - Z)" }, @@ -21,25 +21,6 @@ const sortOptions: FilterOption[] = [ { value: "mitre-desc", label: "MITRE ID (Descending)" }, ]; -export type ThreatActorFilterValues = { - country: string; - region: string; - motivation: string; - status: string; - dangerLevel: string; - sector: string; - sortBy: string; -}; - -export const DEFAULT_FILTERS: ThreatActorFilterValues = { - country: "", - region: "", - motivation: "", - sector: "", - status: "", - dangerLevel: "", - sortBy: "name-asc", -}; type ThreatActorFiltersProps = { filterOptions?: ThreatActorFilterOptionsData["threatActorFilterOptions"]; diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.css b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.css similarity index 100% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.css rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.css diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.tsx b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.tsx similarity index 100% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilterDropdown.tsx rename to frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.tsx diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.css b/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.css similarity index 100% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.css rename to frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.css diff --git a/frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.tsx b/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.tsx similarity index 100% rename from frontend/src/components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid.tsx rename to frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.tsx diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.css b/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.css similarity index 100% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.css rename to frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.css diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx b/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx similarity index 97% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx rename to frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx index e83a091..947d420 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx @@ -6,7 +6,7 @@ import { } from "lucide-react"; import type { AssociatedItem } from "../../../../types/ThreatActorTypes"; import "./ThreatActorAssociatedList.css"; -import FeedbackCard from "../../../feedback/FeedbackCard"; +import FeedbackCard from "../../../common/FeedBack/FeedbackCard"; import { Link } from "react-router-dom"; type ThreatActorAssociatedListProps = { diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css similarity index 100% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx similarity index 100% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css similarity index 100% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx similarity index 93% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx index e97ca98..9149a69 100644 --- a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx @@ -1,7 +1,6 @@ import { Star } from "lucide-react"; -import { Link } from "react-router-dom"; import "./ThreatActorDetailedHeader.css"; -import BackButton from "../../../navigation/BackButton"; +import BackButton from "../../../common/Navigation/BackButton"; type ThreatActorDetailsHeaderProps = { name: string; diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css similarity index 100% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx similarity index 100% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css similarity index 100% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx similarity index 100% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.css similarity index 100% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.css rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.css diff --git a/frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx similarity index 100% rename from frontend/src/components/threatactor/ThreatActorDetailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx rename to frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx diff --git a/frontend/src/hooks/threatactor/useThreatActorCatalog.ts b/frontend/src/hooks/threatactor/useThreatActorCatalog.ts new file mode 100644 index 0000000..02d3504 --- /dev/null +++ b/frontend/src/hooks/threatactor/useThreatActorCatalog.ts @@ -0,0 +1,106 @@ +import { useState } from "react"; +import { useSessionStorage } from "../useSessionStorage"; +import { DEFAULT_FILTERS, type ThreatActorFilterValues } from "../../types/ThreatActorTypes"; + +/** + * Håndterer state for ThreatActor-katalogen. + * + * Bruker sessionStorage for å bevare sidetall, aktivt søk og + * aktive filtre ved navigering og oppdatering av siden. + * + * Skiller mellom: + * - Aktive verdier som brukes i GraphQL-queryen. + * - Utkast som brukeren redigerer før de aktiveres. + * + * @returns Katalogens state og tilhørende setter-funksjoner. + */ +export function useThreatActorCatalog() { + /** Paginering */ + const [page, setPage] = useSessionStorage("threat-actors:page", 1); + + /** Aktiv søk mot db */ + const [search, setSearch] = useSessionStorage("threat-actors:search", ""); + /** Tekst som er i søkefeltet som enda ikke har blitt sendt */ + const [searchInput, setSearchInput] = useState(search); + + /** Aktiv filter/sort -kombinasjon som brukes mot db */ + const [activeFilters, setActiveFilters] = useSessionStorage("threat-actors:filters", { ...DEFAULT_FILTERS }); + /** Filterkombinasjon som enda ikke har blitt sendt */ + const [draftFilters, setDraftFilters] = useState(() => ({ ...activeFilters })); + + /** + * Aktiverer valgte filtre. + */ + const handleApplyFilters = (filters: ThreatActorFilterValues) => { + setActiveFilters(filters); + setPage(1); + }; + + /** + * Aktiverer søkeordet. + */ + const handleSearch = (searchWord: string) => { + const normalizedSearch = searchWord.trim(); + + setSearchInput(normalizedSearch); + setSearch(normalizedSearch); + setPage(1); + }; + + /** + * Fjerner søkeordet fra både inputfeltet og GraphQL. + */ + const handleRemoveSearch = () => { + setSearchInput(""); + setSearch(""); + setPage(1); + }; + + /** + * Fjerner ett aktivt filter uten å påvirke de andre. + */ + const handleRemoveFilter = (key: keyof ThreatActorFilterValues) => { + setActiveFilters((previous) => ({ + ...previous, + [key]: DEFAULT_FILTERS[key], + })); + + setDraftFilters((previous) => ({ + ...previous, + [key]: DEFAULT_FILTERS[key], + })); + + setPage(1); + }; + + /** + * Nullstiller hele katalogtilstanden. + */ + const handleClearAll = () => { + setSearchInput(""); + setSearch(""); + + setDraftFilters({ ...DEFAULT_FILTERS }); + setActiveFilters({ ...DEFAULT_FILTERS }); + + setPage(1); + }; + return { + // State som brukes av komponentene + page, + setPage, + search, + searchInput, + setSearchInput, + activeFilters, + draftFilters, + setDraftFilters, + + // Event handlers + handleApplyFilters, + handleSearch, + handleRemoveSearch, + handleRemoveFilter, + handleClearAll, + }; +} \ No newline at end of file diff --git a/frontend/src/hooks/threatactor/useThreatActorCatalogQueries.ts b/frontend/src/hooks/threatactor/useThreatActorCatalogQueries.ts new file mode 100644 index 0000000..48033e5 --- /dev/null +++ b/frontend/src/hooks/threatactor/useThreatActorCatalogQueries.ts @@ -0,0 +1,65 @@ +import { useQuery } from "@apollo/client/react"; +import type { ThreatActorFilterValues } from "../../components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter"; +import { GET_ALL_THREAT_ACTOR_GRID, GET_THREAT_ACTOR_FILTER_OPTIONS } from "../../graphql/queries/threatActorQueries"; + +type UseThreatActorQueriesProps = { + page: number; + pageSize: number; + search: string; + activeFilters: ThreatActorFilterValues; +}; + +/** + * Henter trusselaktører fra GraphQL basert på katalogens + * aktive søk, filtre, sortering og paginering. + * + * @param page - Gjeldende sidetall. + * @param pageSize - Antall trusselaktører per side. + * @param search - Aktivt søkeord. + * @param activeFilters - Aktive filtre og sorteringsvalg. + */ +export function useThreatActorQueries({page, pageSize, search, activeFilters,}: UseThreatActorQueriesProps) { + + /** + * Skiller sortering fra filtrering ved hjelp av object destructuring. + * sortBy hentes ut separat, mens ...filters samler de resterende + * egenskapene i et nytt objekt. + * GraphQL forventer filters og sortBy som separate argumenter. + */ + const { sortBy, ...filters } = activeFilters; + + const { + data: filterData, + loading: filtersLoading, + error: filtersError, + } = useQuery(GET_THREAT_ACTOR_FILTER_OPTIONS, { + fetchPolicy: "cache-first", + }); + /** Selve query for threat-actors*/ + const {data, loading, error} = useQuery(GET_ALL_THREAT_ACTOR_GRID, { + variables: { + limit: pageSize, + offset: (page - 1) * pageSize, + search, + filters, + sortBy, + }, + } + ); + + /** Returnerer en tom liste dersom data ennå ikke er tilgjengelig. */ + const actors = data?.allThreatActorsGrid.items ?? []; + const actorsCount = data?.allThreatActorsGrid.totalCount ?? 0; + + const filterOptions = filterData?.threatActorFilterOptions; + + return { + actors, + actorsCount, + loading, + error, + filterOptions, + filtersLoading, + filtersError, + }; +} diff --git a/frontend/src/hooks/threatactor/useThreatActorDetail.ts b/frontend/src/hooks/threatactor/useThreatActorDetail.ts new file mode 100644 index 0000000..9a9cf19 --- /dev/null +++ b/frontend/src/hooks/threatactor/useThreatActorDetail.ts @@ -0,0 +1,20 @@ + +import { useState } from "react"; + +/** + * Håndterer pagineringen på detaljsiden til en Threat Actor. + * + * Teknikker og programvarer har uavhengig paginering, + * slik at brukeren kan bla i én liste uten å påvirke den andre. + */ +export function useThreatActorDetail() { + const [techniquesPage, setTechniquesPage] = useState(0); + const [softwaresPage, setSoftwaresPage] = useState(0); + + return { + techniquesPage, + setTechniquesPage, + softwaresPage, + setSoftwaresPage, + }; +} \ No newline at end of file diff --git a/frontend/src/hooks/threatactor/useThreatActorDetailQueries.ts b/frontend/src/hooks/threatactor/useThreatActorDetailQueries.ts new file mode 100644 index 0000000..5723909 --- /dev/null +++ b/frontend/src/hooks/threatactor/useThreatActorDetailQueries.ts @@ -0,0 +1,95 @@ + +import { useQuery } from "@apollo/client/react"; +import { + GET_THREAT_ACTOR_BY_MITRE_ID, + GET_THREAT_ACTOR_TECHNIQUES, + GET_THREAT_ACTOR_SOFTWARES, +} from "../../graphql/queries/threatActorQueries"; + +type UseThreatActorDetailQueriesProps = { + mitreId: string | undefined; + techniquesPage: number; + softwaresPage: number; + pageSize: number; +}; + +/** + * Henter detaljinformasjon om en Threat Actor, + * samt tilknyttede teknikker og programvarer. + * + * @param mitreId - Aktørens MITRE-ID fra URL-en. + * @param techniquesPage - Gjeldende side for teknikker (nullbasert). + * @param softwaresPage - Gjeldende side for programvarer (nullbasert). + * @param pageSize - Antall elementer per side. + */ +export function useThreatActorDetailQueries({ + mitreId, + techniquesPage, + softwaresPage, + pageSize, +}: UseThreatActorDetailQueriesProps) { + /** Henter aktørens detaljinformasjon. */ + const { + data: detailedData, + loading: detailedLoading, + error: detailedError, + } = useQuery(GET_THREAT_ACTOR_BY_MITRE_ID, { + variables: { mitreId: mitreId ?? "" }, + skip: !mitreId, + }); + + /** Henter tilknyttede teknikker. */ + const { + data: techniquesData, + loading: techniquesLoading, + error: techniquesError, + } = useQuery(GET_THREAT_ACTOR_TECHNIQUES, { + variables: { + mitreId: mitreId ?? "", + limit: pageSize, + offset: techniquesPage * pageSize, + }, + skip: !mitreId, + }); + + /** Henter tilknyttede programvarer. */ + const { + data: softwaresData, + loading: softwaresLoading, + error: softwaresError, + } = useQuery(GET_THREAT_ACTOR_SOFTWARES, { + variables: { + mitreId: mitreId ?? "", + limit: pageSize, + offset: softwaresPage * pageSize, + }, + skip: !mitreId, + }); + + /** Henter ut aktørens profil fra GraphQL-responsen. */ + const actor = detailedData?.getThreatActorByMitreId; + + /** Henter ut tilknyttede teknikker og antall treff. */ + const techniques = techniquesData?.getThreatActorTechniques.items ?? []; + const techniquesCount = techniquesData?.getThreatActorTechniques.totalCount ?? 0; + + /** Henter ut tilknyttet programvare og antall treff. */ + const softwares = softwaresData?.getThreatActorSoftwares.items ?? []; + const softwaresCount = softwaresData?.getThreatActorSoftwares.totalCount ?? 0; + + return { + actor, + detailedLoading, + detailedError, + + techniques, + techniquesCount, + techniquesLoading, + techniquesError, + + softwares, + softwaresCount, + softwaresLoading, + softwaresError, + }; +} diff --git a/frontend/src/pages/Home/Home.css b/frontend/src/pages/Home/Home.css index 9cd05e1..6b45401 100644 --- a/frontend/src/pages/Home/Home.css +++ b/frontend/src/pages/Home/Home.css @@ -3,7 +3,7 @@ width: 100%; max-width: none; margin: 0; - padding: 18px 0 32px; + padding: 0 0 32px; } /* Innvendig wrapper som holder alt innholdet i samme seksjon og sørger for at det følger riktig z-index og full bredde. */ diff --git a/frontend/src/pages/Home/Home.tsx b/frontend/src/pages/Home/Home.tsx index 9a2ca3a..39334c5 100644 --- a/frontend/src/pages/Home/Home.tsx +++ b/frontend/src/pages/Home/Home.tsx @@ -10,6 +10,7 @@ import { Users, } from "lucide-react"; import "./Home.css"; +import PageHeader from "../../components/common/PageHeader/PageHeader"; // Dette er oversikten over kategorier som vises på forsiden. // Hvert kort leder brukeren til en egen side for den aktuelle datakategorien. @@ -67,12 +68,12 @@ function Home() { return (
-
-

Threat intelligence

-

Threat Intelligence Dashboard

-

- Explore threat actors, techniques, software, and tactics to better understand cyber threats. -

+
+ +
+
diff --git a/frontend/src/pages/SavedItems/SavedItems.tsx b/frontend/src/pages/SavedItems/SavedItems.tsx index cb224b4..0b00734 100644 --- a/frontend/src/pages/SavedItems/SavedItems.tsx +++ b/frontend/src/pages/SavedItems/SavedItems.tsx @@ -2,6 +2,7 @@ import { ArrowRight, Bookmark, Crosshair, Database, Lock, Settings, Shield, Signal, Users } from "lucide-react"; import { useState } from "react"; import "./SavedItems.css"; +import PageHeader from "../../components/common/PageHeader/PageHeader"; type SavedItem = { id: number; @@ -107,13 +108,11 @@ function SavedItems() {
-

Threat intelligence

-

- Saved Items -

-

- Your saved threat actors, techniques, and software. Keep track of what matters to you. -

+ +
{filterOptions.map((filter) => ( diff --git a/frontend/src/pages/Software/Software.css b/frontend/src/pages/Software/Software.css index 69da973..e6dbe75 100644 --- a/frontend/src/pages/Software/Software.css +++ b/frontend/src/pages/Software/Software.css @@ -9,7 +9,6 @@ .software-heading { position: relative; - min-height: 102px; display: flex; align-items: center; overflow: hidden; diff --git a/frontend/src/pages/Software/Software.tsx b/frontend/src/pages/Software/Software.tsx index 4679e60..a9a9d5b 100644 --- a/frontend/src/pages/Software/Software.tsx +++ b/frontend/src/pages/Software/Software.tsx @@ -14,6 +14,7 @@ import { } from "lucide-react"; import { getSoftwareCatalog, type SoftwareCatalogEntry } from "../../data/softwareData"; import "./Software.css"; +import PageHeader from "../../components/common/PageHeader/PageHeader"; const PAGE_SIZE = 12; @@ -92,14 +93,11 @@ function Software() { return (
-
-

Threat intelligence

-

Software

-

- Browse documented malware and tools by name, alias, MITRE ID, or - keyword. -

-
+ +
+ ); +} + +export default ThreatActor; diff --git a/frontend/src/pages/ThreatActors/ThreatActors.tsx b/frontend/src/pages/ThreatActors/ThreatActors.tsx deleted file mode 100644 index 1dcb5b0..0000000 --- a/frontend/src/pages/ThreatActors/ThreatActors.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import ThreatActorFilters, {DEFAULT_FILTERS, type ThreatActorFilterValues} from "../../components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorFilter"; -import ThreatActorGrid from "../../components/threatactor/ThreatActorMain/ThreatActorGrid/ThreatActorGrid"; -import ThreatActorPageHeader from "../../components/threatactor/ThreatActorMain/ThreatActorPageHeader/ThreatActorPageHeader"; -import ThreatActorSearch from "../../components/threatactor/ThreatActorMain/ThreatActorSearch/ThreatActorSearch"; -import "./ThreatActors.css" -import { useQuery } from "@apollo/client/react"; -import { GET_ALL_THREAT_ACTOR_GRID, GET_THREAT_ACTOR_FILTER_OPTIONS } from "../../graphql/queries/threatActorQueries"; -import { useState } from "react"; -import ThreatActorActiveFilters from "../../components/threatactor/ThreatActorMain/ThreatActorFilter/ThreatActorActiveFilters"; -import FeedbackCard from "../../components/feedback/FeedbackCard"; -import { useSessionStorage } from "../../hooks/useSessionStorage"; - - -function ThreatActor(){ - const pageSize = 9; - const [page, setPage] = useSessionStorage("threat-actors:page", 1);; - // Search - const [search, setSearch] = useSessionStorage("threat-actors:search", ""); - const [searchInput, setSearchInput] = useState(search); - // Filters - const [activeFilters, setActiveFilters] = useSessionStorage("threat-actors:filters", { ...DEFAULT_FILTERS }); - const [draftFilters, setDraftFilters] = useState(() => ({ ...activeFilters })); - - /** - * Aktiverer valgte filtre. - */ - const handleApplyFilters = (filters: ThreatActorFilterValues) => { - setActiveFilters(filters); - setPage(1); - }; - - /** - * Aktiverer søkeordet. - */ - const handleSearch = (searchWord: string) => { - const normalizedSearch = searchWord.trim(); - - setSearchInput(normalizedSearch); - setSearch(normalizedSearch); - setPage(1); - }; - - /** - * Fjerner søkeordet fra både inputfeltet og GraphQL. - */ - const handleRemoveSearch = () => { - setSearchInput(""); - setSearch(""); - setPage(1); - }; - - /** - * Fjerner ett aktivt filter uten å påvirke de andre. - */ - const handleRemoveFilter = (key: keyof ThreatActorFilterValues) => { - setActiveFilters((previous) => ({ - ...previous, - [key]: DEFAULT_FILTERS[key], - })); - - setDraftFilters((previous) => ({ - ...previous, - [key]: DEFAULT_FILTERS[key], - })); - - setPage(1); - }; - - /** - * Nullstiller hele katalogtilstanden. - */ - const handleClearAll = () => { - setSearchInput(""); - setSearch(""); - - setDraftFilters({ ...DEFAULT_FILTERS }); - setActiveFilters({ ...DEFAULT_FILTERS }); - - setPage(1); - }; - - const { sortBy, ...filters } = activeFilters; - - const {data, loading, error} = useQuery(GET_ALL_THREAT_ACTOR_GRID, { - variables: { - limit: pageSize, - offset: (page - 1) * pageSize, - search, - filters, - sortBy, - }, - } - ); - - //Henter data fra querien, [] hvis query feilet - const actors = data?.allThreatActorsGrid.items ?? []; - const actorsCount = data?.allThreatActorsGrid.totalCount ?? 0; - - //Henter ut data for filter - const {data: filterData, loading: filtersLoading, error: filtersError} = useQuery(GET_THREAT_ACTOR_FILTER_OPTIONS, {fetchPolicy: "cache-first"}) - const filterOptions = filterData?.threatActorFilterOptions; - - return ( -
- - - - {filtersError ? ( - - ) : filtersLoading && !filterOptions ? ( - - ) : ( - - )} - - - - {loading ? ( - - ) : error ? ( - - ) : actors.length === 0 ? ( - - ) : ( - - )} -
- ); -} - -export default ThreatActor; diff --git a/frontend/src/routes/AppRoutes.tsx b/frontend/src/routes/AppRoutes.tsx index 597487b..7b36cf7 100644 --- a/frontend/src/routes/AppRoutes.tsx +++ b/frontend/src/routes/AppRoutes.tsx @@ -1,16 +1,27 @@ -import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { BrowserRouter, Routes, Route, useParams } from "react-router-dom"; import Home from "../pages/Home/Home"; -import ThreatActors from "../pages/ThreatActors/ThreatActors"; +import ThreatActors from "../pages/ThreatActor/ThreatActors"; import Software from "../pages/Software/Software"; import SoftwareDetail from "../pages/Software/SoftwareDetail"; import Tactics from "../pages/Tactics/Tactics"; import SavedItems from "../pages/SavedItems/SavedItems"; import Techniques from "../pages/Techniques/Techniques"; -import AppLayout from "../components/layout/AppLayout"; +import AppLayout from "../components/common/Layout/AppLayout"; import TacticDetailed from "../pages/Tactics/TacticDetailed"; -import ThreatActorDetailed from "../pages/ThreatActors/ThreatActorDetailed"; +import ThreatActorDetailed from "../pages/ThreatActor/ThreatActorDetailed"; import TechniqueDetailed from "../pages/Techniques/TechniqueDetailed"; + +/** + * Oppretter en ny ThreatActorDetailed-instans når MITRE-ID endres. + * Dette nullstiller lokal state, inkludert paginering. + */ +function ThreatActorDetailedRoute() { + const { mitreId } = useParams(); + + return ; +} + /** * Håndterer navigasjon mellom de ulike pages på nettsiden * @@ -28,7 +39,7 @@ function AppRoutes() { }> } /> } /> - }/> + }/> } /> } /> } /> diff --git a/frontend/src/types/ThreatActorTypes.ts b/frontend/src/types/ThreatActorTypes.ts index 888e183..0a4cb57 100644 --- a/frontend/src/types/ThreatActorTypes.ts +++ b/frontend/src/types/ThreatActorTypes.ts @@ -1,129 +1,232 @@ -/** Representerer én Threat Actor i gridet. */ +/* ============================================================ + 1. THREAT ACTOR CATALOG + ============================================================ */ + +/** + * Representerer én Threat Actor i katalogens grid. + * Inneholder kun informasjonen som brukes av ThreatActorCard. + */ export type ThreatActorGridItem = { - name: string; - mitreId: string; - alias: string; - status: string; - country: string; - motivation: string; + name: string; + mitreId: string; + alias: string; + status: string; + country: string; + motivation: string; }; -/** Responsen fra GraphQL-queryen som henter ThreatActorGrid. */ -export type ThreatActorsGridData = { - allThreatActorsGrid: { - totalCount: number; - items: ThreatActorGridItem[]; - }; -}; +/* ============================================================ + 2. FILTERING & SORTING + ============================================================ */ +/** + * Filterverdier som sendes til GraphQL-backend. + * + * Alle egenskaper er valgfrie, slik at brukeren kan filtrere + * på én eller flere egenskaper samtidig. + * + * Sortering håndteres separat gjennom sortBy. + */ export type ThreatActorFilterInput = { - country?: string; - region?: string; - motivation?: string; - sector?: string; - status?: string; - dangerLevel?: string; + country?: string; + region?: string; + motivation?: string; + sector?: string; + status?: string; + dangerLevel?: string; }; -/** Variablene som brukes til server-side pagination i gridet. */ -export type ThreatActorsGridVariables = { - limit: number; - offset: number; - search?: string; - filters?: ThreatActorFilterInput; - sortBy?: string; +/** + * Representerer den samlede filter- og sorteringstilstanden + * i ThreatActor-katalogen. + * + * Brukes både som aktive filtre og som filterutkast. + * Tom streng betyr at et filter ikke er valgt. + */ +export type ThreatActorFilterValues = { + country: string; + region: string; + motivation: string; + status: string; + dangerLevel: string; + sector: string; + sortBy: string; }; /** - * Representerer én tilknyttet Technique eller Software. - * Begge bruker samme struktur: MITRE-ID og navn. + * Standardverdier for filtrering og sortering. + * + * Brukes ved initialisering av katalogen, nullstilling av + * filtre og fjerning av individuelle filtervalg. */ -export type AssociatedItem = { - mitreId: string; - name: string; +export const DEFAULT_FILTERS: ThreatActorFilterValues = { + country: "", + region: "", + motivation: "", + sector: "", + status: "", + dangerLevel: "", + sortBy: "name-asc", }; +/* ============================================================ + 3. FILTER OPTIONS + ============================================================ */ + /** - * Representerer én Threat Actor med all informasjon - * som trengs på detaljsiden. + * Representerer ett tilgjengelig filteralternativ. * - * Inneholder både attributter fra ThreatActor-tabellen - * og relaterte data fra andre databasetabeller. + * value: Verdien som brukes ved filtrering. + * label: Teksten som presenteres i brukergrensesnittet. + * count: Antall registrerte treff for alternativet. + */ +export type ThreatActorFilterOption = { + value: string; + label: string; + count: number; +}; + +/** + * GraphQL-respons med tilgjengelige filteralternativer. * - * Relasjoner med 0..* kardinalitet representeres som arrays, - * der en tom array [] betyr at ingen relasjoner finnes. + * Alternativene grupperes etter filterkategori og brukes + * til å fylle filtermenyene i ThreatActorFilter. */ -export type ThreatActorDetailedItem = { - threatId: string; - mitreId: string; - name: string; - description: string; - threatEntryCreated: string; - country: string; - motivation: string; - region: string; - firstSeen: string; - lastSeen: string; - status: string; - dangerLevel: string; - xMitreDeprecated: boolean; - - /** Alle aliaser og sektorer knyttet til aktøren. */ - aliases: string[]; - sectors: string[]; +export type ThreatActorFilterOptionsData = { + threatActorFilterOptions: { + countries: ThreatActorFilterOption[]; + regions: ThreatActorFilterOption[]; + motivations: ThreatActorFilterOption[]; + sectors: ThreatActorFilterOption[]; + statuses: ThreatActorFilterOption[]; + danger_levels: ThreatActorFilterOption[]; + }; }; +/* ============================================================ + 4. THREAT ACTOR DETAILS + ============================================================ */ + /** - * Responsen fra GraphQL-queryen getThreatActorByMitreId. - * Returnerer null dersom MITRE-ID-en ikke finnes i databasen. + * Representerer en Threat Actor med informasjonen + * som presenteres på detaljsiden. + * + * aliases og sectors inneholder flere verdier og + * representeres derfor som arrays. */ -export type ThreatActorDetailedData = { - getThreatActorByMitreId: ThreatActorDetailedItem | null; +export type ThreatActorDetailedItem = { + threatId: string; + mitreId: string; + name: string; + description: string; + threatEntryCreated: string; + country: string; + motivation: string; + region: string; + firstSeen: string; + lastSeen: string; + status: string; + dangerLevel: string; + xMitreDeprecated: boolean; + + aliases: string[]; + sectors: string[]; }; +/* ============================================================ + 5. ASSOCIATED ENTITIES + ============================================================ */ + /** - * Variablene som sendes til getThreatActorByMitreId. - * MITRE-ID identifiserer hvilken Threat Actor som skal hentes. + * Representerer én tilknyttet Technique eller Software. + * Begge entitetene identifiseres med MITRE-ID og navn. */ -export type ThreatActorDetailedVariables = { - mitreId: string; +export type AssociatedItem = { + mitreId: string; + name: string; }; +/** + * En paginert samling av tilknyttede entiteter. + * + * items: Elementene på den aktuelle siden. + * totalCount: Totalt antall tilknyttede elementer. + */ export type AssociatedItemsPage = { items: AssociatedItem[]; totalCount: number; }; +/* ============================================================ + 6. GRAPHQL QUERY TYPES + ============================================================ */ + +/* ---------- Catalog Query ---------- */ + +/** Respons fra GraphQL-queryen allThreatActorsGrid. */ +export type ThreatActorsGridData = { + allThreatActorsGrid: { + totalCount: number; + items: ThreatActorGridItem[]; + }; +}; + +/** + * Variabler for katalogqueryen. + * + * limit og offset styrer server-side paginering. + * search, filters og sortBy bestemmer hvilke resultater + * backend skal returnere, og i hvilken rekkefølge. + */ +export type ThreatActorsGridVariables = { + limit: number; + offset: number; + search?: string; + filters?: ThreatActorFilterInput; + sortBy?: string; +}; + +/* ---------- Detailed Query ---------- */ + +/** + * Respons fra getThreatActorByMitreId. + * Returnerer null dersom aktøren ikke finnes. + */ +export type ThreatActorDetailedData = { + getThreatActorByMitreId: ThreatActorDetailedItem | null; +}; + +/** MITRE-ID-en til aktøren som skal hentes. */ +export type ThreatActorDetailedVariables = { + mitreId: string; +}; + +/* ---------- Associated Techniques Query ---------- */ + +/** Respons med paginerte teknikker tilknyttet aktøren. */ export type ThreatActorTechniquesData = { getThreatActorTechniques: AssociatedItemsPage; }; +/* ---------- Associated Software Query ---------- */ + +/** Respons med paginert programvare tilknyttet aktøren. */ export type ThreatActorSoftwaresData = { getThreatActorSoftwares: AssociatedItemsPage; }; +/* ---------- Associated Entities Variables ---------- */ + +/** + * Felles variabler for uthenting av tilknyttede entiteter. + * + * mitreId: Identifiserer aktøren. + * limit: Maksimalt antall elementer per side. + * offset: Antall elementer som hoppes over. + */ export type ThreatActorAssociatedVariables = { mitreId: string; limit: number; offset: number; -}; - -/** Representerer ett filteralternativ. */ -export type ThreatActorFilterOption = { - value: string; - label: string; - count: number; -}; - -/** Responsen fra GraphQL-queryen som henter filteralternativene. */ -export type ThreatActorFilterOptionsData = { - threatActorFilterOptions: { - countries: ThreatActorFilterOption[]; - regions: ThreatActorFilterOption[]; - motivations: ThreatActorFilterOption[]; - sectors: ThreatActorFilterOption[]; - statuses: ThreatActorFilterOption[]; - danger_levels: ThreatActorFilterOption[]; - }; }; \ No newline at end of file From 2d1a9717f2f46e3f5b489cc5690e17528cdc738d Mon Sep 17 00:00:00 2001 From: nikitita Date: Tue, 20 Oct 2026 16:18:57 +0200 Subject: [PATCH 2/2] refactor: clean up frontend structure and standardize formatting --- StrukturMinneLap.png | Bin 63584 -> 0 bytes frontend/InitialSetup.md | 76 --- frontend/src/api/graphql.ts | 2 +- frontend/src/api/graphqlUrl.ts | 2 +- frontend/src/apollo/client.ts | 19 +- .../common/FeedBack/FeedbackCard.css | 55 +- .../common/FeedBack/FeedbackCard.tsx | 56 +- .../components/common/Layout/AppLayout.css | 2 +- .../components/common/Layout/AppLayout.tsx | 12 +- .../common/Layout/header/Header.css | 2 +- .../common/Layout/header/Header.tsx | 28 +- .../common/Layout/sidebar/SideBar.tsx | 17 +- .../common/Navigation/BackButton.css | 31 +- .../common/Navigation/BackButton.tsx | 46 +- .../common/PageHeader/PageHeader.css | 29 +- .../common/PageHeader/PageHeader.tsx | 32 +- .../components/common/SearchBar/SearchBar.css | 89 ++- .../components/common/SearchBar/SearchBar.tsx | 59 +- .../RelatedTechniques/RelatedTechniques.css | 11 +- .../RelatedTechniques/RelatedTechniques.tsx | 24 +- .../TacticDescription/TacticDescription.css | 2 +- .../TacticDescription/TacticDescription.tsx | 6 +- .../TacticDetailedHeader.css | 8 +- .../TacticDetailedHeader.tsx | 11 +- .../TacticMain/TacticCard/TacticCard.css | 23 +- .../TacticMain/TacticCard/TacticCard.tsx | 16 +- .../TacticMain/TacticGrid/TacticGrid.css | 3 +- .../TacticMain/TacticGrid/TacticGrid.tsx | 2 +- .../TacticToolbar/TacticToolbar.css | 7 +- .../TacticToolbar/TacticToolbar.tsx | 11 +- .../TechniqueCard/TechniqueCard.css | 17 +- .../TechniqueCard/TechniqueCard.tsx | 71 ++- .../TechniqueCard/getTechniqueCardVisual.tsx | 20 +- .../TechniqueGrid/TechniqueGrid.tsx | 85 ++- .../TechniqueToolbar/TechniqueToolbar.css | 2 +- .../TechniqueToolbar/TechniqueToolbar.tsx | 36 +- .../AssociatedSoftware/AssociatedSoftware.css | 8 +- .../AssociatedSoftware/AssociatedSoftware.tsx | 11 +- .../RelatedThreatActors.css | 2 +- .../RelatedThreatActors.tsx | 11 +- .../TechniqueDescription.css | 2 +- .../TechniqueDescription.tsx | 14 +- .../TechniqueDetailedHeader.css | 4 +- .../TechniqueDetailedHeader.tsx | 18 +- .../TechniqueOverview/TechniqueOverview.css | 2 +- .../TechniqueOverview/TechniqueOverview.tsx | 13 +- .../TechniqueSubTechnique/SubTechniques.css | 8 +- .../TechniqueSubTechnique/SubTechniques.tsx | 11 +- .../ThreatActorCard/ThreatActorCard.css | 33 +- .../ThreatActorCard/ThreatActorCard.tsx | 132 ++--- .../ThreatActorActiveFilters.css | 12 +- .../ThreatActorActiveFilters.tsx | 35 +- .../ThreatActorFilter/ThreatActorFilter.css | 3 +- .../ThreatActorFilter/ThreatActorFilter.tsx | 104 ++-- .../ThreatActorFilterDropdown.css | 1 - .../ThreatActorFilterDropdown.tsx | 32 +- .../ThreatActorGrid/ThreatActorGrid.css | 5 +- .../ThreatActorGrid/ThreatActorGrid.tsx | 32 +- .../ThreatActorAssociatedList.css | 26 +- .../ThreatActorAssociatedList.tsx | 92 +-- .../ThreatActorDetailedDescription.css | 2 +- .../ThreatActorDetailedDescription.tsx | 2 +- .../ThreatActorDetailedHeader.css | 6 +- .../ThreatActorDetailedHeader.tsx | 81 ++- .../ThreatActorDetailedInfoCard.css | 4 +- .../ThreatActorDetailedInfoCard.tsx | 23 +- .../ThreatActorDetailedInfoCards.css | 9 +- .../ThreatActorDetailedInfoCards.tsx | 21 +- .../ThreatActorOverview.css | 12 +- .../ThreatActorOverview.tsx | 24 +- frontend/src/data/softwareData.ts | 20 +- frontend/src/data/softwareMockData.ts | 53 +- frontend/src/graphql/Example.tsx | 1 - .../graphql/{queries => }/tacticQueries.ts | 7 +- .../graphql/{queries => }/techniqueQueries.ts | 40 +- .../{queries => }/threatActorQueries.ts | 37 +- .../threatactor/useThreatActorCatalog.ts | 162 +++--- .../useThreatActorCatalogQueries.ts | 98 ++-- .../hooks/threatactor/useThreatActorDetail.ts | 19 +- .../useThreatActorDetailQueries.ts | 138 ++--- frontend/src/hooks/useSessionStorage.ts | 34 +- frontend/src/index.css | 2 +- frontend/src/pages/Home/Home.css | 20 +- frontend/src/pages/Home/Home.tsx | 45 +- frontend/src/pages/SavedItems/SavedItems.css | 5 +- frontend/src/pages/SavedItems/SavedItems.tsx | 81 ++- frontend/src/pages/Software/Software.css | 19 +- frontend/src/pages/Software/Software.tsx | 537 +++++++++--------- .../src/pages/Software/SoftwareDetail.css | 8 +- .../src/pages/Software/SoftwareDetail.tsx | 150 +++-- frontend/src/pages/Tactics/TacticDetailed.css | 2 +- frontend/src/pages/Tactics/TacticDetailed.tsx | 9 +- frontend/src/pages/Tactics/Tactics.css | 2 +- frontend/src/pages/Tactics/Tactics.tsx | 4 +- .../pages/Techniques/TechniqueDetailed.css | 13 +- .../pages/Techniques/TechniqueDetailed.tsx | 80 +-- frontend/src/pages/Techniques/Techniques.css | 2 +- frontend/src/pages/Techniques/Techniques.tsx | 31 +- .../pages/ThreatActor/ThreatActorDetailed.css | 15 +- .../pages/ThreatActor/ThreatActorDetailed.tsx | 181 +++--- .../src/pages/ThreatActor/ThreatActors.css | 2 +- .../src/pages/ThreatActor/ThreatActors.tsx | 202 ++++--- frontend/src/routes/AppRoutes.tsx | 25 +- frontend/src/styles/dark.css | 0 frontend/src/styles/light.css | 0 frontend/src/types/Example.tsx | 1 - frontend/src/types/ThreatActorTypes.ts | 149 +++-- frontend/src/utils/getCountryCode.ts | 24 +- 108 files changed, 1919 insertions(+), 2006 deletions(-) delete mode 100644 StrukturMinneLap.png delete mode 100644 frontend/InitialSetup.md delete mode 100644 frontend/src/graphql/Example.tsx rename frontend/src/graphql/{queries => }/tacticQueries.ts (91%) rename frontend/src/graphql/{queries => }/techniqueQueries.ts (51%) rename frontend/src/graphql/{queries => }/threatActorQueries.ts (71%) delete mode 100644 frontend/src/styles/dark.css delete mode 100644 frontend/src/styles/light.css delete mode 100644 frontend/src/types/Example.tsx diff --git a/StrukturMinneLap.png b/StrukturMinneLap.png deleted file mode 100644 index fe901cdb6e4d42c6dd46f15b382775a4a47a7ad4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63584 zcmeFZbyQW|yEaUx2qK7-Yy<>E>5$$Cf;31sh;&JJN(f3yNNy16?gr^@kOl!^(;(fv z6XW?k=Xl09-ap>|zT+6%z1VB5IoF)`oOfLJb+14<88I9z5-bD+1RU|_A}KdPHHv0XCQINGHA`$noUVNF$rP{lUfZ~6QE2l6HJeJ`@AVOh zv{Zbux8gDM^c3(pVn>Ltdh!Q(-F!_>U`3-Vt*7&LG7B!2`)HQQIYf;4kxQyNfh2@F zCi*?OwqnGSA$;33n3t59v(B?-6MNaT1E&b^C`VBj4cslkLnkn?K?bVghSJgq55X}8 z0tzAt0xCE{1RnuJ(tnOc5$O<+fBueyfDmAUfb#nq8Ss7mFARLH@A>tO9N~|E4*tRi zAE#ubKUbsRB_sbiM$!i75QG$j#l^w5g1(J`fu*gnm0cWrx)AsQ^YwEzTLgqVG}j+Q z@fXxPp#3ouMO8ahX(=9kD+@-wS5_|#7@aI$U$=w6=fndJEe!1RAWjzMmbN@j{P%xe z!2^!3e`dN5`FV+*8UKA%X*r0nm5l*}laZN``Mv-a1Onl+d1c7+LPYfU?cgu|`^I*5 zuX&i59334Q9oZPIY>b##xVgERm|2-vSs#Nd9@{!w+UYqxwzQ@C)ySWAL=0^8ZA@O< znOIpuuG`gnX=QK6fB*h=@k{_9TF|J=#K%+CDpTmNlfQ4|W4a#tzoz2XJpcR^Otb(NAJacV6TrHi(LD|1NM<4;s|dcqD!cxN z$OQhQ`}KW&9CU!?!&`=c07VcN5mIzQ+(<k-}gW)Lc>Yhws1Hm4j)YY;i1e9^mcR9-Aq1QVn?%P_8wEHxFbT&`r(Gh0?kMplMK ztg+u?tgMb)Ke%NXz7O1Zhl7OYg@6J1lsHmZQ=?QlK+;{-d2@xnj0k*4|f6W-w*x!`5#7mp`qp$cRe&*8_BYigEFRxQ$w77BtOFKNMru& z_t#jVx~uqfIiWh)UUDUd(j|>(q>ehdscQ6E{ zfG_@}mc|~Zs~I9WY>{*jcJuLpQT1CS6aP&{*Td~#LaI8xpuxge&#%eXm=#)oRxI=% zDgK@iNSF@F+SfcY>ibejd{rYsQJ_tep%!T!)ly^mEaLFLZ$R+QewOTVuqHZuxBhf3 z=LNR*C$GXmQPSAJ+qBVfn3(^=lVrxBM4Vn?U?`Qi({ubatYl3L2qY3)l-2vM>Hp4! zh~j&0sbh-%*NUJcLc+jU@(GvvyT21@1VqY)shG*Xhuh_cfb_IVm-_bK((t$qg?4No zraAnzt9Rf|Is4+PU(H;vHHg< zTX>vR!v!an^aw0cg%E3L{r-s)whv8JQhRlYP6wL^4yobHT~ z1>1=7Xb) zd}59qK;hxE57c?&sp&+qLE5Q|n)6arYA@$hPo1)=o}Yvg=R|ki(d_!i$)ptn<#M4n zXa?r(Sqd4VObzEYcAI5mqCgp{87|SU!_)6i9dn z(qWl*{c3}B^^R3b(eEtmgE_vw?T?AVJ^GR83m#(}fiuh!nfv&1Vty$MQ^!zEDjq)U87^Ajab6GJlQlR_IxPl@zTh(T1i-ea~nH}X8 zbM@}#DpaEHJGxerT(<&;?bk*+(=<%dqVDtVQb&a8iqWET>9C2h$3&-s8(SU|G23&dSchq@{! zq?*AuOq)!4Gfsluucjebuny=Ymfi4#eSbO%H4o zHs5UH5nrxe&u6n;?g`u;tI8{%>|(c?opuAmF`TZnZaeg9AG>HoL?_eDRWQtLHf(!O ztiLgWqVIQOI8&bLi2raUH8f7}^0=9%;%4H6>vmnReONDL0NFy{j59ou@v8F!ZG4e_ z`>FYiN0gE>%anOZu*p<;-pOt^Q!g;1^(_V%6mqGeI58Dnyk=h??Jc}xszz{8hBDV| z*VkOT4)hNnVp*w~wkuOJpKVsax;+ZibXzSjxFF!Ls?hXnuG%ZVq$f zCy}*LS6+z7tm)U1`Gmtc{w1wH&7!&3i06$(l08daN7|wKhq=T?%TfVE@j$$ z2ew+(B=p4`>=s7!BhsG>Ju(rRPlX58dTxn+>M0ZJaK@D(3A?fJ1L?RGQEED-gGV_k z<+kv?d^rlw^P+Ww^2W2C<#?;DK9k5sy#R8<5$qgkD&&zf3-U+$@Nm+Z)Fjtnhn*ZO zV}hssN&LO0IeCVR)hqY)6J^Hs3eNIwo0=y>$5QjO9W3fxk2s06olI_FT z>ZdGgh=WO-76NH|FliI;j<%Lxpc$BE*nK2Q@@!-`nQY+~=5PBt!Ir09#(Jr&K%@P*O^U`_D(v%^XeK38H z$bsiQ?^js8`&rxvS%HbVF}ho2o{Me47FMq4Q+Gyt-xIUF-)P3d4V>ictMo+{L=vHT zdXs;D0RM$y{XW?LRf8NQM*W=emepA$R%wbC2C!n`iG<0iP&^DlNw9Td7VS=!Vrp)Y z6!Th;5NC%IsV6SJXX|?nXO|F5y~Uor7;jb2($9aCP?ExbINt2eSRc0^HKE_n9~4Yy z&9#UAYf0;3G9xu+|A= zM_N%6boyPkwvi|7P|x=)HH7D3{j%#-RK};NGgo#;P5x7!t5Fr5R=9oLXXo9u!9yxs z%esTFk!MY(>fu!N(PqRl+)I&_Nm!Xi&oE~np?C1=pToNG!?{&&=g~&tWn?(Tn*Gt(O`etHujHq2;hfe(vW`<5?>LFfvGUP6vPVk>_S$~`6!?8aiX&P7Ev zdX(3v33sIWX(0T}kj2Fys-&Ga=>w#WUp(~I7k^(BJ#(pxOlqENIu2dgcQRsh_iV=D zYMHy8;swcIuS+awxO9Q*((A&~S$)R{-TH8c>sQm`7tCgzW2gwz$E=?aKJ7j`S*PKS z%@aVX16V+ktEMbQBdN#IzRO2C{}+8J|rT~$HZWq)NJ z$??=|o?l#fJZw0zPtNDVTDB7HP=TMrcUWwFjNF2n#PROfW@hxU+xpi$(BM6f=m;zf ze4BIIjpu=p=aCt{Bw7_zhCy0;1}}9uF2{ysN^gVtc_jVIrs7@O)QF1+?Km8X*_~Fj zLhmkMScj=-)n(jH88lt`urctFM%ELw3siL6N4fbp5j2a;B;r@A9cKySA^d{;Q@pA< z1@$g&b=0(b#odhWw&e>PPrKnq1H!niCrj#Rq{h6g(-yG4;3E&(s)O6t8q{+!M z9Yq)HY84FSpJM>-%(d$fGPkP@U7HSHCh9Z&P+a00U@*ye7GN-4!qCFTn~B1oSN39^ zPETgniVnwY?Pu+E#ATLajgnTobl*%rvvInxm6(L9`vYgFy2a(x2_X#=1OM4Z@y9@I z5P2I0+GC1Ad|B`|Y+{N&xBz!6?Rz)_18E0qpCk){da3a(S*5D|A<0EEZRpNPH+Q)= zkwQ{p9>0X?l+;mX@G$Y@626vNX_xS6*=Np_U6)2E%PsT5&f_|qT-990c_Vt@1)Gpx z5{rw8zIyqEyT0M4C8jg~xj=6|43G+P$9(BEQPqUTdF_wNa^msuFHu6q54Q9J=w>>x zkk|Z%@7vhVwhG#@a)b(+e@Jwgq-SgfS~t-~diATSKF-)}io=yc|6*pH7sLy zZyYG+jH!m`nIJt?-J;>|djK(BsiANw_!fcH2Zu#5Fi4-t)d`$@|L{^lm0fm@w^)nw_{V;( zZuAorDtgGTZ{PwqjgEj6BQa?g53l7h87mW9Y_FmYIjt^~fQh+@(7 zVIfk+$8ig-+PyUfTIJ6C&gz!M~08rlA=*zX=z;Tc&BVk&OQ)(HKA*v zmLYVrVr~|PAqJTtLrQYi&p9XKsQzs0o!g}Zl~}n>gNOWS5Q2a-qc@q^*IZ)yj#Sva z$GT7l4wb6oZsF+xGUkKipm@3IwMeX1s zj40GUrs;{v=Yc6b!{u>){G0{9$>z8?`fIs>R(YMz?O6b<4F=LCRkoZr6F$&(`_Y|a~TC)(}+cM6Sxj^&;`4>8+DvGl4+J4xnxeYju4J;tUhQDw%P2{of@j=CAUA7`u z94<9cV6&R#(+>!Yr;89&uGatoS5E@B?R1Wtt#*0UQd3t50kcD8T@n9&Aa>UIrYV}? z>Hg|6NHkY#b(K&}&i0ur(eY(DF|JCD0mS$XT4Gy9b7Y*$$yEc_1XAds+;BQYpO zvNguvuo*jm&HTPDj1c{WWyzs|QwCmbkN<^Y{EqscQUL(W__2&T<$r~O0SsmU0jJQ6 zr;+}D*i|MzO14st;o+R8=icSnHW2znxkRM18leraHyl^zd!fW!mYRW~(;&X>J(}~h zsz2ZD#visWvO`+Wt4J~#&5m*gF@S`u=1uywu>vb)brYJsVOhag5Qatxhg2C9HDNJ) z>`Ua0XVGu_IOloA|D(5B)&Rz8AhF7xyCu8yrC};g$#m=>&zdh6&Tb6>`gdd+a;*Sq}Rnfg<1xe>Ck8KwyW5) zjS|`YNwb1y3+lh5mx>Wr(GW6Xa{CeawDj(!3BNc85aK$PSCy`Nj_U*SD-%Yr;1CduyuV>tIq$!6x?T`kKuAdVtjugIWKLy?N$a9e@Ozl>i?#c3 zsXoOQw7p}VWuwaPK%f{F5z}yI>lR+Ir&T~g1Q`=HqFyNf;p@YJ2A!)+{q_*~62sn4 zw`KVT-3_Vlj)G+|!YTVAhT6pD7T)2ktCfl+COJOFtA3mP1GR=9ZKSCHuWMh@et|fD zc@`i7PqMX4(21xfkRU$cd6)4dVjl12g0Daztj7WKQiy&j*X~6FmAtK}ttP`h4w7@BmdZtjpa72%p(tPwx8M}>?w4w!>VfO z&TyTp;~s#fTE}u<>*iMKmcn=)^Laf%K=={;p3|52_xLC4Dg=vrfw8qzuduZEL}z20 zy!qBIl6$i2>I*Wi-QFmGGD-IU{tyeS-b0T*U^UDfrme-U)#q%TE9q3c)#0>l|FMfx zve6vaW0u-Q%WuYV)rM=qJ~RCG#rs-M27QR$?|EE4-jo0!8_T}KT(y$-+Q?`(m`SZ_ zix}W>X89)R((#6O7&VPU1Wt`hbQ_Tr)cN}XU=wqh9&Z`cT?l}Pc<|{rFP|`6e#cbL zJf`KCShXByra5D=ZJ!`xKVv_pB@F`W-hf3x_GYmgTofM1dTuj3h1bfP(ZOf(`BFTy zo=+7>m-GV98$YAlPB$aD(s}PxMv)jmi`@F;^^u>mE6CHW%BO84+4S3P*a8K?Q|}<+ zSG7^x{bI=9$__T!eP}s#W;p5K)Hw}0WW3`mtMY=EPI{<1uW})1`FJsu>l!nYe-zSA z;W>Q{)^)C9!i+L8`==X9WZLcBHFU$bKZTW z|4eSL37e_XsGIZbXxplf|47qlDtmDxOCgvLr#FmJSd-++2q3}C#l)jx)>5<1aB?Hw zmm@hUOfgcMR8w06^=~FD&o-y8lEo<8OGmD-0dRPAHh0w{!@K#NLM5M*#Hb4qAVuCt zv_GvU-D}~LQ#>!|30~NnJpdkZXGkG=&*j%0s_y~ltD0i|GQq>ETP+}Hrt&2k2~L1L=0tG9H6$3=o|*!NhQ_e;qgRUeKaw6^ERKMb$2 zQHPJ!s}m&|#6T@!^BB1%Co0IR^V2{(I%{{Du(jkhN{#15h(|SgT7-OjlK9QXncJl? z9j7d~Y{9ft+3L6-L>2K*^@YdmW$V71+z7K>x63tX^?~&Q+hf5w{aTm`E_FX7Dc(YX z1d|k3@Vs!5Cg8bD8u{0IC065wDf01>Z{?Vqc!a>f}9{?%}tcF(bZc^U3 zrqN9hrmG8?5dCTG<4A9Nu_B2RmO1D)_1A}{&LCutrPh4jN0?&)H+i4r9A zuKZVQD%#Fz3<`|p=|&RC5zy#Y3R+*aT`k~lUEvpCIVZBYOk1~eTW@XEY&*#0ZX_#Y z$h67OGsE0=1H07?6wj_eQaC?&l+h-{0PcReZ@rK}gjavEOu7ed<}E4!X#W;lvO_ev z*$3(IEOE7N3o*+l^P)#<&IW{d)c)q+Qkj+_s(bIvF93?`09&LkkI8!+F&^AyuFv>! z%Bo@J8td*miFs}d;5VM-hcO#+jS{0bt?CYW+3<}?p#^jZs9b!tY#Ry^Kue{1P51j|(d|~z-2^ZmU2XO{H z9KWzIym2oiDpSUitE)_i57v7_*S-bY>U6y;lIR@$-jgwA%U+zLqr9@O>fz~p$5r#? zVgkiaMP4O-c@k`&Ijw2#8EQ|hDNsJfEVEdUw!Mja(!t|EznYu+&8PXKR6HrGG+Q#_ zE{6D;Nn&|N(0%KwT>aY55b#O(>72jP2LW zx51Ig?s5J3$xf6iFm2Z#W6TakH~VuEL5S6QEKsK+5SPC&%wamxQFKeplhNHOQI?B9 z;0CpEYH!WSp@2n%!}AwhQ*0ZR6SwfKopT3nP0Sv5@C^OnwpP;0EyRPF4W|d!j0G`n zX)=}@J}d4xgtj|pX)ok)V(HY^XTM9LU>z*mET3X5_IZ&Ix)3s+c}{OF_G&CUbZu$R zY^vNsqV9qKjp22xiQ-M(Z0h3sVs4BIRqjcX>$d6rdG^IK{9lP3x4TJ1 z89Rle=7|Cc-2il6T2?#b7D@uV@3kGyg~BaVdCT8CBLiypg*AoX=Kbd9#llrHB^v{J zM^`F>*-LVMM+UrKaZd)+*c_j0R93N&gQ&Ey2pBAG!SYb66 z$<9+?6ip3T;Z?RTYHp!gq8FXtvrqU;9x>7za{ui{$Gev&-QhP;QFxzG7Y}Q^Q0+=m zx;atO75Oet+_*?V^+l3dhFv89@3ir6xYa4eT!&>I+x=&CWM;ewC%P1|=?@t%NEqhYiy<-BnixrnZR{3j7PM z2akA{?t2utemK{8PK4JrCE8qV-D5>~9gDyE3Dj89aleY#^^@!P2887LOGq1XM=K!7 z=6H2`Gr<^6mOjQyfM+K?II|h13zy1n*O085s5)oF&6D~@o+stX-Pa`?;_HK*F32$K zdJ(3wQ@ayK_Gr6<#yx#Y_92AMLi}LlKn}W7_i?rF-H`YS2+YY!q%_aDx!J45LQJYZ zZ2KWF9n)zY?>e7_LoVY)N$5PegL$~zj(1+xZSWKzxY1x@NTkYRIPmy)_aFJ3h2IdL zq7{3uVW0BR z)iE_}jCtNWAim9o=|$rF$;H(n_Huhg-2`kDR1|*RshmfgYu)Vv2Uiq)-@I`Ml74!) z0Uv}P{2)SUi%z69*L{QYy#dEqwKm}aPGdRZ?5Dhn+3NADc00tzDL;R~?xHc%n>G(D z_8 zGU}M(9aYVxkl<3|CtI!KZ=)KxKIp5*d4=_QM^sXwi>6XUHlR>Qv;64aJp(DU21Ufi zj3|AEdGWpkB62q^Ni$%`6qrC_QZDyB4+RzmCuaa@UAUzYWw)lev?SN$H(z2gmCUV& zuH$>KEXIZ8{x^5coa%_|ENeE)UsB;IQuL2(D_MC8WO?bnIefNV4U-PVU2NP|0~AW=(_CV5*P#%K8sIDYc+QsIHz=9^{s|eCCtvIcqQMHx{y1n^N=t@j;f+q@n9@orVkJrzWTlN`c325ib5nKD*J#_jn*YaQLn(Z9Cd) zTPm&*{hakn8d|XhA5T~(%VTM17{+IMG#gzvfk}qtH=;qu<)ek>X~P5rFqxx{&fS+@ zf)rb0YDS5wzB_Q5`^Ibjj^iIng(Irz{k^(w%e05}W~m4g;B91M$b8wgSEy3F(<&79 zJoN}{0GF79X*>TeF&08a6~UKcaMsaQ&-5k}bo^VdTs>ZSZ$)Y42+SLG;#R0!L_Gk4 zK=!TX#K{op5K^w0`sOb)%>$BH3x|wWh?H5I24?T59a781DYPIg@M<$6*`*z%rybB6 z2HU0z7q~;N2dC?GI4-N{^(zUNOyXXcQ=GqT0;=zBxqMU%(KV`#K+1dz2}uXTjq)Y( zI23i_%Rq{7BHM?p{WGQvIp_ec6E|$Fad_Gim2j9@ms_kpL#Z!xD3eI3FVd&8Q-0_< z>h>c8Tk3PRX7_Z8-BqNg5k?PXYd*jHd@Av1;4Nn2hdp|ur_IAlpL5=euf;M#j`bGe z-sQVY89dZVZ0*O!a7n^7&7>R+@=Pu^ z{oP@yNKOTU#022Kur)lIsBdg zB#Q`-edy^A{Fc6arv(ArVsahaU=h6AOEg=iFb2kkrr?2rD>fM8KSE2>ORayu!8S#E z&1fO;$ALgzokT1n%iX0wn2dGEe@D_GvR2ls7y?9R&)s$!o=-7vqxya4b3rF;IGAaZyn)34)TD;PU;$Bk0`WY24}JKoF17U-T=_nr7@Pp(@Ix z4ZlSX&@7NEh+mCi}riH6;G@s z9SmbW*;zbt-RU3F4oO`=TdD1$slj%=vMu6RqV;~Zxw4+cciW1M zBX#(<3Es=uV?N`FQt3L^iZjX{X>nP_yeQL0eMR6Pg< zc{3_TTb=)zHA7Q%Hvf=*!HUZ-i=k7#TRK>nKxTC5iAq!nNJdu37#N3=n8E>w*T^#s z=>V?Fwz&~QMS`TD1*JyZt^n+NY>MzL0YSTkixcXDeUl6AkaUW#+x-TU42h0jtXet| zwyPm^{gTb!mry$A_u%YU!cFb;8438Xy)o9!50Sj$ysx@=FA`khB}>?5QrQKP^!CfUBVz&jG763i#Z&5pt`rmfh1(VQ+F1NSRnPg!@)0r^3PBtWH6dR1 z1$Nt3kQL``HtU6!|3J%LUR1gPC-IcR{&#tOzKjpjwDEAH4~Ha5bt^v(PAsqxINM-* z{umhv1ho#R2>L3#s%x?(snXZLb5D$pN7V>SNglTl$)`vm?Y)-7s8l>>-lw2O+n;c* zF7Yh@=1@T%rJ~KV{DN>AP~y0(EI}ey&zz|YAV{7G#~#~tN6Tez)F#x2+h~~IpyP6K zrVAS|s7E~t4K~s>Pe9SnK#yG=&QR&(ku(0uu6!;7Xqem#FP{FzA;pRYEX+nue!_si z7@yR%US44dg!F#{&8U(Hh$*`9NkPJYwHE=Xo6q}#BL4=ieP{qWT_`Qv`gePsm=FlH z(KCs^nV^k;T1PjYp0V(Ed*1=wGos`^&EITKUC|wWXbyF1Uy9s;rLY2Q>L`xKL_dk0U~vPwG#*~IVU4`$uP6J$ zN+lQPg%^ZgFZUKB3B~pI^IqZs!ZNJ<^MhYqdZ#^2rbcXhhmL=v=@tf$sEF#rUZM2g z=di95IC?A2%)b^|vMFB21IYf$VDrk7pAWMZ-6Q726w^|+wBIWyQWWep_NSV^_b0&< zZ@Ie|^3VB<0W?Uq4dGABHzVb9dhJBo{?BmbuZLSYPsjPI2VJ#W&xz2A`MFH0&5vw< zv)2&3y8tyi#ijx`?ai+xs`$kFa}#vPj!>Ht>rZM48hgzL$0X(YP2a`}fWFX!m{d1W zib?<8#wX8!YH67KFbT37*H6%u`0ElICJ6B#byFw>nt61)hY#h~J^?0-nh-4_J*S$Q zj{EmC6kcyyhcksgQ<{YmfrTtS9CZrC|J^b~jvid2*kza^_j@XXsKV;cUsf}}?tWbH z5m(?id3^W%>gvOMG(O_r8`3*X2mw*gM2<%Gx1Ez-5hZ z(5WDb^PK4`zZ)(F@}$G&*!oR_|IUrbMDScB>|=5-g{vMVxey)uXPrg zm9XjWX;XcOQu`LqFiDzE4&)HR2!2b(`y5C<%+o?5`1k5s5`_@llEBB^hobn}G|Pzo z?p#6~2<8PRwx7>$!Q4Z-9$OZu@t6RKcF9L0$wF_Y50dy@qyIk^`M)%?ZY^Uph=R9qgEB zddqu%SC0Jm&GawT_=Oc%he9dqJ?39>`qwGwC^gtH982c(#m5vBdCS@GthE}V5)kG;-6m70!r zU02AAzMd>KG5q3%xZKM%r6b5#X1Ta#?NUzwZaQ5?sI1y}4MZ;kyq-N(EL29kpY+cROFRfugvv3>mjm|r zdYSprYXM%_U%U~(2`cigwqF+o>rR*5=6lBqU}FW7a9NT8tj}@~&;ju30Lf84hEXee z)iIY4?^BGnOV)@HBLG_m3-y{cs{m5HlPUh;!5~O<;m6c~3>Uq#Dq>?AQ;x+pV1x`- zS}#tU9gM2Tc4|1QBQ|OQ?wQ62VAdOh+$6{<;sH{K*i; zTMP;Pv=-7D`ahZFrq|5!EG~bDEnqIK(s#NjLi1OaciVsAj_g}4A(B-K z0)Tqmj5Uv{zeh-Z0=WwI;?%nfZv5CxklgCEZ2B<(K~vD)Ej1o&N)&^By9%fvJ{+U1?`BZ=-c4d*QoTolGUZhWT@Xg3{8w~PEHOT0sUe_CG zje#)Ms(Lj&Uq!3wjpMg6kab&61xZFumyM$K`@AB8*+6F{6f~$jeFiA;slXka#Uxnm zKSc6LRK7K1H!M8}l42_;-mgWxyYiAQ_Fdh{ziQg{u7F-%Q{kUi(cHpgpVaI5$Pz9} zIlf9b$bU3_d#oD{lZL7S4m&};TaakoHU>=UknaRjC+9MX1MEJ{H+dhq zfMT&!z`W(`8=GT*Yasdf?{sIyqyl=CBs4738YEoA(8=xOO6NSzo(cQkuzvdV4H}N7 zN4$Blc-GR_$oC-C*xs*$kEjRoe)g&CpmJ6mjp!5! z5!(Eht{X*S)aX-i+b@XdA_euQrk@9qc;MTk9Vjr-vulPqCK||CJHTlm9YR)P`C1A2 zCd27l38DxqN#;v8_eMJ~Tkq|_qeY-$cs8qPG1av#PpKY(?B>c7D}JhYuh%WN#43v= zbLDwJb=8%obG*LnuhlJg$r<)$8+I{&4T~DczA-eyA|3x#QE|lyp@i05;A6YFnmzQv zy|Z!@*Iux$jq*G+=G*qDHWfbm1#}qO?g|j0qiw9{ZqU{3zTlC5H{gniu~u}{^ro-V z1TfJA4TKg%Tu9Qeb!?6au^V9wIbMZB5)9m;fF%>O#{;NqTHnNB6^Gi;CxAYY3e1hD z1e@FOYn6&&Szi8nB1Fov#Hapb&DTUTfUG35|Ki)R1xPJ0?F?bH9coW{l=vBsP9LV0^z=1HTpM%dkC6HINl z9fmu{uB05Mkv0XsNNJ<&h)b*b5Z{w^)w;{I-mxOB^1+$ZD59a^lOvBfKTjQH#71-> zMZ9Oybpzel$n$%3UddUcAUbj=05u^sqj%3SZbqRZQVI(2&gPE~C;eoIJ<|C3)Y^q) ztevOAZ~XiFi2iI;K?~hP`>)m^mFX0(O!f1~An@7t=R5bI)qufvgX9(@MJNZaLr-}? zcqU!|DFwiW^!*XZ$#$1+_=rM6I5aQl(B3dri1Hhsh;y%CX=c*Uaf~+VFy(l?2I=JL zU6V4bE5I`Wd3eA&X*+R6q;w}lyK-v~gLd@yM_B0`mNNb1no5rSB%2aZUr(zG3Fm)N z(STgpc97Xkk2|Gx--~P+p5)57%Yt7F#GHtmflWst_>)ELe-zL^;W+1>$i~>N4*vj< z0#Lj2Wd-B~15vT^j+4Ixs?Yw&&dCqbc3DlMBiod_gP5Y%>@Slk_ocnChez4NB}p*U zOYRAbcy4*a#P+8|qw9WR#Y~zvV_S6g*&Q5`e$mf#k^S_MgGsp@80c=0^RN#^qDdPW zgFMhkgN`SBd>hn7{BShlM4Zn|4Y8rZyEcCv=!$ND)iQIj3Y5XI-?9<=Cr`-aKVMl36L5WNZBg0q6{`CHdB>;>wfjv{wxxy}p5b-&BPa?8i}~mg5Ye7SOZA1tz|2z(KeLn5HHe z80C+j^3LDA_nWdP*^LG&G)FHVd(9BenLjXGXbEg(dX4tS+h{@AU`Nwe(PM77IE+65 zfWYlJ_-%E{#+Bpy=m?Pk5J^e@{cS3A5T;6?scr5nwa%>?5`Vm9ckPWu=M|z2l?xe z01Y^VO~yv1f5-8v_d&!Y=^GmVw-f9K;)fLG$yoJ2vCN;)2mvMfE%1-==NAqCI8^`m z$zWKSrASW`tL=NUag@0ZlrycObF^xh)!+P2&3Ju*ftc0V)+nBM*pmf&xnIX8A!tsW z9@UqFG*6!-w>&-8ih2(V2DVD5%bky_3A^g(NUnCT)aB*6jgWj63Q90vrm{Xa z?Y&6J&5qw8+rPS#`-1XS&B$V}eOhga6+dMN10%?Jz$w+xaOiUZR;5W6&)-qTIfk*s z8xh|ETEGuyx%DvEYrr`1Kw2>mEfu$`U#0`|FBknAiZ7~X4~K%sxksR}A_-#3>VwBU zObwT!Us`BPW&Q{0e#Rf5^Bq+&Gk)Oy!+On+Y)%Q>G;zlwwmwXMG5mkPXMCZclBXx* z_N-(CyBP@6a*ZqhHOAkE9k^g&oD7DHApP}(g^Jffgi`IRUoeh?1p#;`#jke({Ckf7 z?_2oayZ$|&|L_Dh3$T>RjxH>t~{uOZpp_0c-G~9oYy4 z5H-{fz?hjk50VkCpW|UP6a;@#03+(LIV_`7e|33z2CIa6UL3>(exmbec!bMiIoi0- ze`J2t@s6oxsP^EHw|n;ox1gkfQY@V5qt)TIgK^(c%#uH-3jVk@Y6fZ>M=U*!dOsQV z#&K%dk42jfrx%afv;nN1))j5y%K&_r0=VY!y>~6i1$P-^JnzM&RDewe11)AkrkpqeVU8ZH_6+;laRH7QE(W9w$pag?Sj>E+_0UvN!t+ZhVM5t2g=3wmg1M zEY^B)mQ|MTU>|>JZqw_c$9_z6b@$2c$HiVrrckv{=RXZ1l=2!FZ&A+ZyI!D3;9;*Y zF=Rtr+V<;q?mL!QCRQ89=T)%shj0&>m#;q9n&yV<82bzCgz;)mdg9?p(2`+cNVpI8 zR>bBMRMn3MpWKHKHFmD+y2)X2-67|gwtJPDelRF#jAfHve_X+6jTPMe8UI`e znu!t&y#+z=ekcnJY8MwJ;We66_zEQ`=<+t+R-IMm>eTi^{hp0#k=_!hcUqgUYFLUk>@Lsi9f5f|95EOtF0U9(u8nHUn$K7>3}-LhF)X%! z2y;1ArN05ffc)EOoR1!|ZhJD%ww84mh)6EtypyVVJXL+a?#Zda-qil8^$&JF zEzZ&0iS%^sI+x{cCfYdMHc#XF)6@7T(nH9-M-7%=2pOGWwoG&i_b$~cCbZA%4##`z zW=fsS*o?L`Jv%oYSBEB6*gkL0hldi{wv0ZICgE}z6XV}YcxO;_md{&rw78jh+kJM@ zVxkR|MA7YCjE?iDRH*~ZBFDl)+d2E9MH?0>W#KMFnZIKZwaM@Dc?i*<$XIfIocuHn5nv$5) zjiOOu1oIpWWO{aZ4?jP-U!`ZN&Njrtt1|Hw$6q6}!4necP(=V(88(iO$w_9aJOr^I&GHICWqTRTO)zOtIL0sJ-p-p1ZC z$y6$dWf1Laxg#nCGFtA!PyyB+c2)b)Ct16ZE5)gaSK9I{cneMAZ(!+C99j-D-91VC zyaHMgUav2?jbf8%$u!=_F2_;49?-k4R|(!5Y!qH-KTIMQ{=PTR2=Y7{Wh5*A5%=}yJ6+6qsy**+Y1^<oeAN>n-#PE@5G+^9nt8?Deli* zRqnIaF`v@d_7u^k!Fe?CD-%U z+`Ak4NX}|^U0-zS-k|B%&ZL*F5FLA&6`4c_X0yDiCvp>k4y8>B4kJFC2W}6)B#1H? zf7rWf47lB+yjj6cIx&bn*;acR$Gy>UZ|lhiufi=Jvv}HjiZ&uSyY89MTDPVR4bxW!gm_EpWJoOQ61v+ZT@HVjq`^TDPO9qVUon{b^T9y2bRglK!b zXL(de03Y|ax5of)%NScsR?!4N0x$E`>$G4cBK}I;I&D# zq8n!9_FBW~S&tS9oOdP(o^RJscoe1*P4e6ls_V0nX@&&7^c*9v_{hj>Z)3ZxdGxA@cc|6T8qHHWTO3tl-F!+7L_%1UR6cN^EhCvs)wlX)5Jb?V0vosFt`$-|km( z-pY*-!5^NY_vMEy)UYt(D~v6ZoY&-4N_o-e;)`AUP{NO9U3#@h-TK|r=bpvH4mc~j z15A$`9_A_NTx>E{o(JByQ7%a_nJ-gX0V~lH-m>1@%iCP_y!0D1Ogla3&98B=ei|og zy5ahz@dteMV`?Zf(b4+)ZbY+Z79j(B7Xax|w=~NI;fLJA`#e9K98UlhpS7U=u=)p; zSgC|xI-hx2Z;0V+t>B9_R{KfzBTr#ij#d51*AN-!F-_}H4)EG2@w>TS3sb7Tfm&z@ z{PpTvw;oPca?NoLr(bM{GR<%wCz+4V^Zkr=KHoB)EDaod+e|aTIcZFF#%uX9OHHwa zzBnH>n*BpW(vbPPa?5cOag)klp>NhNyJz=Ic(-g-%yDlyzKU|#s$nlbUgPSJ_sU&w zqYS=fXmxfvh4x}N!u^ybUz+O9tlI@4+!xn%MbGvtH@s#~jNJNT)&2tZOp!mXj?-Y; zmZ~RX-O5wjk??@3aBT95%~uaJ+NrdlPVl;;o<*tvUZd|d+YMZ26ZaG-!nK6^bSU;d z-DbY$z9G&P(P%V#5oWo(#&AbavhF*{LS8R629LnO`;uNNDvGg`@aHUF^CeQ!9uu-# zmh?_l?qSV3_D#G?w0PFPZ9;B`8va3neaGLdN5FM^bx5h@;SGE|PUGTO%z#9ggCWJ< z4pnFss{tr@3>N7rHNgOyolN)gd{6dBn@ZBM!Tv|4*XzlyH^N(-^#PSvtugU6xh45| z_4Xn#mvg-_P8Y!LLwDqb+?%mJRq?0T@5i)NBn%{2j{gGqaxg*IQ_N|@Zh-S5h@CyQIBLlx1L1EMLDlsprn)W z(VHHn7@dXLZG+aDAIh%*Nl|M6{v1YY!JtlQHgY?(WwX$;Vyn&5aL##8sRpT0UB+pu zJ?N==h0J|E{UCGD3R)fbGk1S_@anD6g8cuGhG07rW=3j-F%HRm$b!N_h8nWjdm zKW|!q29T7c$lv!E2}McdwIp4MaGkob{;7XR&ApF{#2L?0;Qx^K-ceC4P5USe0}MzQ zKqLta5|x}q(g1=;MwBQyN{*7l0FoprNX|%51eJ_r1Vpk#$shuPM9KNrqrT_7$MfCq zuKVY`cP$r7W)FM!uCA`Gu6pXJ-dmL1A}vi;V_2rln5vTxh4=QzomB z?H;n@E-zt`pYzb*VzV*hoi7*76pCD^&p26?*s(fzhaa9+XeDFq&x{t&H-x8OSf{<9 zA6HgtI9l)e?Q+xYhlMU|h@Hw@j3LIC)i^7JGaCWxS2Zqt4eq2GuH4;ApJy0Y4?f-g z7`j2-v)y>so;pAG0|12mbPXjY@3*P23kXG;sM)W;b2)0}?3bStzWx#nzHX%Z~>; zUaO6%S)LJD&7NP&-0j%Rwm+-!dLsIn&eV=f!lO%h1~xHVKx}Fr3~32q=jIQ+?31NV zyq!Q>x&3jTGL+L*vSjByZgGF(t zY#)|6kuzOfpIPg_F6wK+Rr|bg%x$UH9;)s$HxtBdn-)&C`i+Q__$8-nzu*lv-|$=E zx2fxMJ`qKT!bfBouXtVdA7~Ugui6&MPzF_GhXa{cSQ9Hze7mZ=*n_>P4c#cAwt3kyolVy5`$Uiu}29 zDui*pVc&24;2A|G@6mv|!+vk=+FI|k9oCER7T0spU8U{M^Qs{QlWI121mpL4+RHH= z^lYbF^e+_1dtgd!kr0WSY?b3BcAPDH8_*AKAG9s$j=_MU92I%)E-dNyt=|+qX&CJF zK6t)mAl?|U1^^R0U#DxkqeY+J7;s{FfQt{Q-fx&1=snvX5!C{CEOL>BzRxc%b&b7% zuDl(2vvZ4{Zn2d@#B0KRyv-aQt@$~}4ObX@t}#uy(+Wrbwf&m$Nv%UQ)Bf~1vQ9U& z@z#~j#7w_$a;RWPAVpj}mv};e9Nxjb3j+5~6DHO-$IK@!N#Q$_1(}q~wu-_pSoKhX zGrk+XxlgWa)E)c1$QhL(o?Kd-vGlPT|LAV#cTDFYKR&kV00QMHL<07!id=DzxoTRh zx}i6Jysu;JBR`Be~co zOe)gKGZ|k1#ZeBjn#gw(Yms_z5#hvblS?YXBy`!+!`=P6MQW9D_ElSi{t_=g%vtEDD6~_xo=-p7Qi<0yiq_dqlls`!eeUMvV76k4tmn5mi~$)S#s3 zf@;BKt@41Z7Ar5638CWqa9M1P_ztGGw^Pr%m#@3NdtH5`>(W(R_1(2oXn8wk(o&80 z0+Hm(=ZoHIEjthUmt(3Z-{eGET$JZLIYJ(A3T>~L!@~*bGs}qKgQy;ubuB* z1Tm29s(1AiQ9~It!Zr&g;z}p0GFAR3;RQ+?8olzj8{4UiQuk;4rqaRaX4Y%=CJW5Y zHd1XNfvQC$y!Z-*>b%aEb{TI?XxpSt5lgnSvRA4)kYPWr`ySl}35TGpZ%Z|DzNuU= zbvSTgUWTKsg};zh<5ap&J7fpI+li{1xMV#0d}Vz3 zCcO1WtvWy6`2H$=AjDKo@_PbFJXZZ6mv<}4vD>_pc(l+d*Y(i_67lYJ8tZtuz7->!VlLq}vQW#xQ*u zmGUbgtDLVCb+Ez8xqGID><}QoRv%sW6TF7{0@5}S{Bs1rV@3sD0SCFvfGnhiZ!!Oi zV!2&v`|uXp%83!|=0Oo<)mX;->|Teu8{=G$D>G6>8z<+6NOFB!}5W%0$D!v4VOf;T1 zdkl`Z`>U1vi%g7P1N0!JuH#O}9uIES6JrO*CXes7P8TF;eQ2=PtYT#972W3$r8RO& z3$-b!{WR$~$oXyxAG)NIJ{(yPk8w3DfBh;nzu%de=ZL=U!6oqy4zgXJ zTO-HfAGrpX6AAAT9}Nm+Jx_Bi=ExD*xLp+ihu^LA#+|Wm9dLN55nAkC-t|CbtKd$W zefnL|``)KWit7TlxZ&pM?^Z4Rl1_4KM_*@Uf4W9pzuodJaVm_qO|qXaRoBaH^}9pT zdMNGMK&TUvj_xq23^R&NjRtVm4HVnvEKpUrjk$5}i`?y(EjB8BNl1Dvb!F`P*U$OA zH@5Fm+`P9;J}R>%-YEn8$J5yF@pRj^?`=qNqZKW&on@wHib&0V_!g)04mAby&DQt$c-L+FquXVB5UviQ8EMF)ya{7;(PtgDh`}^gVp-!pau_p`$&T+U_ za8;vN?Nk3)cyAu=lF#DDTRn@Z!tNg{ARp#cqn$rwpL#S)X|T#kN_M^~{n5$Q6C

^DSvZ`%zn)m?}TS=o4}n5e*t2zzTd&a01v;}XhBe#D8q$iI7;J!u-m zB-g#7aqoNAr^jVdKQU}k%Pdn7PPdJfRE|+QCZy*b*lGOAx7}O)_ThD(QxiyFMy@(G z?q_qcT`_Lz+pg3e+lcL^H*;4p_oF5EC`Ad*ZMP>J2KDVP2%Xl6n(lTuT|Y=9|@B4wKczm_23b?ovxD< zW6aLs@!~{V7w@|Zdoi843mmoM%$kDwS0|Z}+E*g$G~*2Y)R%zk6vk{X@SrZ6#*W8ryu2Fsw6TEu@Dm{Q zd$8D>3{DoOPnYiViiC}Yfc%r;YDbia%VNAPEH9DllYD%3^~r;zn43>Q&AhggS?PsIN&`j7OIneY*!W~zq9>*Cc;a{0kohcRRTVDQg zC{FOB#_a_S6rqM*M0fWt<@liQ4|YiiYZfv?W`q8+2IbkNyi2>zV=bRBRn0l9My8jq zTeO6Tq(-rbxP*?7qfQydvunZVNGK`-O#?(S&Fx@5=wdKtcUM2D-P zM0${vL}*Z@0Hi1AJAe<z$_%}l2S^&@xY~<3De-Gm&8rJ`^f^y;CME7?wfPC=-dKv#cjC)bwhWBiEJ1_h> zKzJbrkS{q#`mKKtNpir#* zGWq*X4wnoUbm7ooQb(ct=FDY36m}Klo0X znWOKonyoK9Q1bYfMDL{rXj&eeeaK8KBn5*wNU`l_TO)AzA0{QUNoz(~Cr1H+cs?hC zl*ek(`}3tcE`Z6A;uo68DNYtxE}B(Z4~4;M_~(iK{9@q=(Dy(gX(Y_Z@!yPLFrd~@ z9ylCAdHLV73;~b8B!C8E;Dr7q!~bYE3ayA3fRQMmnc+Vw{nwWeU`4>2kpHiITv>%@DjvB6yk#b$)(1vF&W!XZ}d@(Kqw^;%3Z#>0% z_^eAN@DrMn42OWy2vjzl%{)?Y7682MgE_Jn_^tX8+W^+j%=Ij|24t8fSIhb&(2O>> zYaDvhL@b61)fWLDl2f3LhZ6wm3j^|X#{x8;Y-`Bojb9ZlYBgwmTtflvnMGGaATvk` zE$XZ1-iOkNaEu%HpSleKE(`o3b6FT{g*?G3TOAZcvG|F6Iyvd;14(u{ImRnYM(7?J zi$JDmg5we%p?jyzUwlArJOzi4y87h_;Bo`FvfCDM%0j&CCDl2i!Y+$SE}vv6lF?*1 zckWejKw1D3nFA0Q-{8x8>{=9rtA{H^x9xADR$ldG2;rFk94Rk=_Be0s;$kw$HUA_Q zx+Vn{zZJ`0i{(covMYhJ2NX6KS#&2EYwEk*Tbf)2Jx$0L*|dTK9J_fxBr!%`Zvo7S zj0JN$eiHy`-$Sl--xzSHnY(}&NCnK9t%fF2)$Fw|YWm zB{c0}x^&Y4)F$*kSqIe);qQ-KJX~v-0OO=%JC$V;sCeiSKL@CD*UH?!ndKi5@J0W}xOYvUC?fV15OP~6{H&Dd-OkZ}tp?T+L$z>#&hH{lR_zOK2_yD7kIE+_LHmzd5rgn;VKY9$uY^(Hj#zNAWQ@Ed`X)r6+Z zkD_G2vo~Yr_3(yhn~s|2f|?)TZnr4w5pwDC1rr?62I=3y&l}D^dH>=f`>QvB-{&+p z8j}2gvb8OCJ+76z^G#xE=8dt8=%cJCGFgEmrsann453MA{xFdi@P-Bh6ZhZwogO9A zx=((3CN-!g{iNOUIVC2Tw%Szb91?~I;VjMvhu}4Bm<~EF4P7$HCLsdLF4RAGqG%m- z5a0zo=y`{L5mO030hNJ{bk})qqtQd@>SBr%@EwAR@O%RR1^CeR#kr8Hl+_Lt&s5{(GahofLN7?eY8ovhVGM+<@=YB?qGy~>a;XZaGR(QU3$RD zXU|pxq3{Ek=Y02#={D<^(}z zyC#Txzy25yn>|(qr&TMD>NJfgI81ogS@H5!<)MCYP$A?tNFh+A?=l%@KeocWbO88{ zTzg;iv6~fmby1D7%*pmHbUa~9pMPawBXT%b`GL+m36S1*wc*6ZGJKMC`OaNE3n>F5 zsiE`9G|=D>z(#b-yk$a@djS@8bquZhPI-?dvu1tLMcwzR65eU~L;@cBno1wQ1FWFD zLDtb=W?A|Su&MVbCvS}o>;qE5ATmsJdmQang*#^t*TzNUNNHM>AyF$_-n$c^N~jwg z0mxOkV*?h2vDxp|5h{zr+KSbX+AckUrUf%IiT0w@I*#A6LvPS%r9R4uX}sm#HqrkL ziie{rwiT@YiqH-QBf|1WyA5}#(A%p91K1p|qdm{qThJj!jwL;!*HGxLMc1b|johq= z<=!gyz=|8FFIa_3KA`$fg|fc`dl$oI{au<7y}F@i=i!~0+O!rlO|{i}Z6JB)q1Ht& zk%^VI;RN&om9q!S=3mE7GwZh_pT5)`-`q$voQ4)eP1Sbn)~tUE_EV0${bdNur0t-C`PLG}weaVU%) zO`4WL=lPYS%ggHX8{=_6sc+j(ixt*D7<|Fe;xiTF2S?u{@l9KWsF&2RFmD2;Kqsd) zop6)+4;@lX+tbl>;y0pbMMtL1r?I9izd&?&%i@GLu%bfxytBH*9cbd?7sBQN|^3U)pDt1Spnv!la-{RpDGF*v{_m2z!=JhJz zo-`FoT^;S-2SFW55&?B49@2JD0(_p~xtJ^fYW|an7>{%8CMqrHV!hVC#Opka#7_D8 zWURl@-(PfOj8|@*hLIWP@m-a!uQKy`5pc5g>x(R z!5A{B=nvGCVTckjJGy28^UjO4{1i6v808K#em}*`=bfQI@4k6spY8e4?svepe%LT^!yG=p%&GR9vwrSl=81QUG>fSk z&UOY>2SNSn0B||dj?;8~sn6SVEYF|nEQ8XsKAz*%)o;}?7(F=c@4*(6SpB;CZXV4A zGqV2GC)ZaBa|ZzJq^L}1ZD!1B2q0kiyn5Z_B7bY<)iGRwT}HBOZJt!sWhnvzsF%3f z;uB8eLR&X{>MJI$v3LWZn$bgnKcEBo5vQI5zJK#x`^M6H${Yuo`+lDYp`~(Y&!%?V zfdKPhOJZkrj6aH!w?)Doiy@O?I8Tv6FP~Rp0{At0aztZHX1jMUot2alZ_tm4^1-h$ z=5GKMy=gSfh`?eHBcp<0h6u@m53*9=$PQi?6>8zK5;~YRl(?)CHhlef{YsLYi>)PA zHt;$__1t}L@+=|c8RRbHmGIX_8x>?^j-A~h1e^~h3b7$EO_94Rk&mB-V`AZahLiMk zk0g>tK64Sa>Tgouuqp{HWP<00`K(`HxUCivFh&}L;|0nVBkdg)_~ zX_}Q9u`J4Y4PZ)eC`$--OQNHC_nrp%)NvVV85z4JMJ7cto0oF+^7lT}+MSly$qt4C z+q}9Sa!Kq4(=81GG2Oy4^p^9ARFZvRa!F}1Kgbm<4@N=%6dF>ht2@+)No1JP+T%w5 zO&_*eBgs7m@cYuCx4XB6n5feA?&~f5a302vgF=q$A&t!*egC__HmU9GS=mc;m{CX$ zMosaEa3cyYGq%`i)iM$5!EhgO@&ljQ8FN^}&72EbUW|?s5T_1X-np{vo)kev0Usv2 zbgFgRSYs)T8zv9$G+hnKdW9u}nDKQwvu%V17y0-4@{4Y)h7i$O=h0QlEaLJnY(8X& zNNyI7#Y2YRED%E3S>LQcp41CvI{8Wz+})X0X1OL8q$M=S;lz60LruLGGhnWhm&7V!gED5X$PS=(D`)sfF&hM>ebg=3PyQKbxM zjk@-?Mp47FMlz3lVm>DZ^!dmJfnu6e5+t?QTuTIt$D!tfZ$mHQo?*U$U_@P94h=A3 zDMC?uc#*d@QD$)j?D%>y_XoVai-j`BBY8A5EbK7NF&fm=r@Dy2-x!(xjIrY`+rBe+@a~wz=&wU=;!pBR(zRaap;-9fwNq;=$eXq|E zfwyV$;duWZI0tNDaCZkvE1Dk(ie12(&jkeJdroh+yc2ND=_tts_ICIPH3gdpK3XDL z>B#+j{r2nMIgGz)4&je<({UZ{_Q+^d~$A$v}gd-li?QT_?P+=*~ej3YgB#ERnVG${@4>(|c zj4{_sAVNh1HOGmEekHSUePJo76p+BFm-LN&7>SI$LnkGIDY)~Ngl^Q_Ctl-?bPJ$9 zZocK2>nBSlSBhZc&%%Z1@0Cdj`&%!N)DU?B*k zz%mAGy5&j%pZ%LPrk4*?A0UOQh5FtrVgvcCZaq3PyoZI_etShWRme_B?LC#tHJUDt zvPi*nCYyjCWJfCSwsvBFJ{1qSj2A6Vj#-yO2_E_2v;^akb;BOFlPppNW-LD2n#u$> zAx%lOr#2G5!NxCAkRz2JgQpSJh^N^%OXWG=s=dhgzY40rQA>zM$xC>5Uj6wy7!GDx z-3}*0`7&=GU+lCiFa8nm(vsy(^Pjy6cA9UelDbLzVA@P3))R>XQGbbQYLJlYq-3)@ zSSfma@L3aS#0=%5;lVx7(uqyzeTf$tRelaIdy5YXT(dz6L{qD;6Hg$5spFR$F=Xam zGrAx7WF81`vRPKh;sbKlb1*322Y$*OXRh^6Sd6&6b;M$-N?d(c*o6EdagOMWE*+32 zQL<6W9|OKP{VSQMCW;fz*a~C_emfNlbOW)mi^8Q#;=f@rjGUdEzW>BdsRGbtk4|$E zkWweLhb@r$aA7eFe5yq2S@$v23=wR}gvHR*pZ`PYuN@^dk>pXFhR|5*o<-EWF zxF$CpcgY`A%-WITX<8spnjh=9&&=#|S|A*q4d+m6KJ3_=H8j|^ae{MwC~wx>`IZab zOPWz^EKMS}uJVfFK$a|PWHk$#ln_Rpr13oM91Pk-alULpTR?P2I0yLTb%seIVz{Lm z9SL)%-Ij=0Or$6NsM+P8jXe3xa{CQ~EVAkD@xnVyW=~BQ@QJ56ONkDSx3Ypex@b?z ztPngI(kPvU{QKFyJ2hx=g*8pG0`tX_@Rt|YnT7=Q(VT?z&9~qAWFy1)bGtYs^p|zS z5x@v)h){$;QI0gASxR1pCAR2`uib18;H=0pkVGpd_Oz{O)gf&wlN<~6{pp?xJ!EAP zeXRR(c)CBM$X&M?nD<_5q;--$#YoK*{HCU?wwe`OY5SX;;M|n5@J6cCaaKWA$9( zeZxjEH|BJiJ;*GTU9$~ov|7g~5cJ&VJs@p=H;9} zMvJUb6^SEl1eEZ%&J7ts4sY@VMzMG{NF#a0%m{e~Gs(l-qS+UYLHZ&Iv11!@C;H=h zP$4rdoeWCn0UuLoi2NSq88I#&pSCyxzuD)wce#&}zev`ov-M7>h;O=xr$n&ZX+-HG zKy*XJf8A2+VPWU4hrOBq-8~`?JYpP39u~A6Cm9yc)g85Y+(y}?Hh;=`97;zW|JL8(=aRCIuo0Db;DCPJ?^j1Z!1YTV&$m!ciS3FVzYtG+|os!N>6@GfWU&Tjx z9OH-92w=R)(Z<{9Z{sbP1a3rqvRU-0xK9yBTCnA%9?8%9DFZ=xobbUR1a=YNPmzQy zNUbAKyqX8XMmqaP)REFv2JKMoJ5QcILXj7_m?fTb%ufvTqa80K^ehytUFgAIfBo~> zzea%mi9`U!Yy!UT@8iXLU?f{0r zCh+fv{u~1o9Bd#2e{JYL2m9A^Zz9nCxDopHzjn5NJQo2$0a0PdfZJy)|MQ_g2LMCA zGUfjVUK!Oys>tyK1Xk0)bU#CT$(zcab|7LVTnvcbmqCn+^ZF)eW61${;2o9$u?1VT zs`6P5(2Heorck2y-BDL=&+?}DsYbWaYP9;7RFKIe)w6*Y2JR?wK703YEyWM?1*yw* z00rvS({-Njesr=A0&qmD z&cF^JwJkt%pR<_u*P|CcGKvZ9>OK5RGqSp2fD(#v7;vUffdJ`)uN_bLqs}DH4^`1z zBm++sxpbJZvK?j`meD(!lmO@eX}t_0K1zTHOGyJn^OjSFmxI7qxr;%EWufg80C{+} zR{YEa!BN)Dw;UsRCJ!h!?8c^-CNEbL1lQcwTDpW5m}e?_Vf*~0-Y7msbdSs>=sJi+ ziVl7g2GI{!!2CV7bnhdwa2cmX3E8z1bcZ1Ljzd&wG$=A}7@8{T@#BNP?8zO9LTi5$ z2N}@n*uR&&xf}*?VuH{;D@!4tqn$udumC_VrW*RFVIcNa?~AVZ$_MRp3P7ig=YZ3o zm&g!wxYGb+U4s$qB}wQbV9`0KwbCOU!>wH{Rn&brWG~pPS9QdPy!-mFF>g6qsEWJG*i&J>IoX{&t41Y#O`~6 z?i*2rGcJaqv=6mcW*YsiK~xchxGJ1qobaH#APiC%`ZSk3Kw)2!N0W+-o5d*)fX12j z;#hQE-?Dc=o4m386QmivV-u$4iqv9@oWfwVDn+*A}>=Q_F@VN{$_8sH9U zT7NP_hmL;M)7RC49z2#QJjT-0H(afbz%2PDUL=H0Gf$Uw0fwD@aT*?d*H5y8%ED7- znSDf9>v$t4gmtyv03i|x3LSkf01Q9e zgj9S??zmxjfC}ex>jr2ubX^7yYfL&pvF^uwe!L%b+Yy9-^%k+ksd@N8 zG{V9HcS(9j*k+`=TK!1Mh%yaOK`et5j3Q{@A@BrG3yN5vwoG#h@%_*crb7US(wd@Z z@`!eFo8%b)xP(U$9?z*)Dd5yA@hC^5;Qh7kAi{2erZg$!z`-Wy(jgodHV>~x_i8~C zAlaRSDMlvcB^r!(D#3poUa>(S_C(HQDfHvNJ7DsAY!ShgQv`U)mfwD$5aoTT?$(Eb$z;Pk1IH9wIloZMty95Zv?-7Qcr*b9aqtKr$x1|boB6tXmY2Ze zsABPwmkR!*Re`-Yo~;PE%FnOFLqsj&eA=1g_Fafv+TpnMly_U{%~On)%TkXvnH6~9 z@C}%B^lpBP0O}^vS2CHG!b0HQxFiM!{)^i; zrS1YpV)246YYAuzvICTULL$r%dY!)mHfXy!1#FUS2yxF)I;}x9?VxocGS+_8k z_AI|f(Rn6xrz}0TLLeqTUYV<@?pRE+LO9xGh%dQl1X>P&h@nOBWtVQ=hg7}8K%J70 zjN^aJQ4FeSdjs$!sq}-S*nkXGuX5?|I5LT#K;LvcD_jf3BBlJ$ceEPZM0Bpg{Wq;un@=Z0Aa(_u>kq)dlU+S z@GF(FmvmZh3NjPk58~yuQR0aPnFG!zcxpNbi{!&y0k;{7t!T##;O|p;w}0XPo^}Bh z`R4gW6R;_YU@>&H>eQeDt_FPyd=v92Pcd<)S9Ie!I!0ELe6?L(YSJXb!$Mk&#eM^@ zl~vxsSqg7nl^DGlZ!)kc){$bQbDNmr7$cS7ppb_hB1>teI#fv- z@mFIrBpsWYT%{AqZ1-rl)+~Y(IS-N)j19hNOWr!^IgJyC&)}x${Wvr98(Vm=Qtq#b zXWT|&vFWQ6Ut|Mmocf{C9?;nJGDSk=;Zdp7)m(b-o>h#L01 z#2vK3PGF5Jn|r>;Uq+kAkp={&GbOIO{$ypO$mZgUf)7d^)G!b^@7~pA3#L)u1M5y3 z-+eOFbr)oo{SpYEC5CJv#Ps%8moiWRQc{Njw?>dz+)y3r!@x)=9~mrK#^~uXhG{Un z36Eh7&asPu=Aa{QrzQ02;qy{PX9``b>|{Qgvsx`ax>qgg$*88|xhD?TQC0{6+t2-8 z(wCiDNrbotc0GkD&jkT)Gv_pF%OMGce1Sh{65nzei(%&NmRLg+D!>>eH9xoU_FVG# z7%&6znFcshb`vYXl_x`?KmhGzu~Pdc^-{R3cW7Cow5bMeB!KD;dCY5hryH5&X#zT3aXgdo<~rM0 z+TK8e4o*o^gl#gcIq!8@>?9BO;+UlHVFh~;C=kM*PP<@`#2oCFd+ssU29k!FFcVP1 zC~?iy`d4G^xR+B2C z$?X#U!8{XN9FGTTq@aqaaNWcGx^N!mm^_;)#^UxUnu%kSx^@SXiP&$WTO)&(;I)5BkjJ%EQkn3!_boVYSBfAUe#eZe zzy2JUvfe4)2Rj0{Zu$&j#5M(Fv0}8ez1R3)>JXIfK0OwEm46|lWaCQtHJlc|hj=gcqUO!zPYu8d#W~g_ZAOCa+1xCb5*#xM(tlfFBd{%*F*{`5DXHk5549YVH=*)M;v%JIZB0@w(@B5i5q>{pkB)F z4r(OkyA|ex1KAIFB1KPFvb;dFDk)0fkxA(waKILhsJq#6mP`NJo54q)Hvi$xlmf$4 z6y}gp&5IQg&_fNIykhc{Y6EbTE@>JYjY(Nl<>~|km@`9;F1=Z$En5o0*=ARP6kKcdNK3Lk<<%bS*C!6Eiw#&nDKbNl7esKD|2%SIzqjIibo1+w7%XIUeD&TRv! zuUe3~a+)>>nXLX*SKFKpPacLOURJxAGO8R@lf-1qxkUv5rQ7vF8R|X%_s&t|7m_jZ zc(`3(=?iQ#fJte7Vq{9RDOOjWqMLK4z1jCYbss^_ zy4>W;Cm17q{^J%F>Y_j8m6tWD(V=M_jtlB_W*`@DlR{zdsLfT zYR=O9^3yhvq6$F=se7+3HHP@1&$S?o>WkjaO#j}_a7dBxxVK=RAKLCoi=#rWG~j+o zd*^TABQ7}IShV8M9vblFC`>nn?Hy`%skI!bO;Jm{H)kN0J{9x2(_#JD+fT^X{q@4LO|*Xz^B3->sC1QptVotGBzw7T$WX5=OEwQg8g*1jZA@cdw5dH;syPsPX* zK)(R%UoQaC(+?nz2!cJm`du6yMKxU)SWh$VdY59c8D|Y)qP$dOV{kPal7*JdraV@GkVzlcWg+F3>}VA)%nXC{zo(lljQ#{Dx{`@({c`o ztIp5AJ%yb7hrnuW3cHScc>KfG2aKD@c(o>?Rq3~{Q6}#06cJNX5ye_<)e%$0qxSV9 z^$}s=uxk;RcLD)d+sq-i{&oq!rNzcr+GOaAkHniLrIN$maoh~c5ncY&Wbz!z7lWWh zq#fX7RMAg#pTjS5t$N)^_tbrRXe~kamEwx?r7Hla2t)k+L+v9(rs4R7T-(OD+tp*A z*-*h7a?_vvE5C6t!L4uh^#1pE(SOyQhJhE{6m?+9rma^xtQDPedGhZB7^+vUlkZ{z7nkN~s`|NBSb zebg2i%UH*!d23fpCOD}NXarg&`_5H~W8v0+e-Hf^*R*DjGyN(+?qv8IL>}>c(=F+B zanD5My-w;&41bH_=l|4nAoG5yR=MqGn)A|e$PUEn(A)G1dqJ&s-Ajyre7FAq+b{DV z^z_H~B;BiQc)?d9%Wtv;zQ6X*=cD8})$}-tPh_7WH%X~7smY9|g^FL9cU`JWXLujh z_Jv{+fC`1UWY-Y?*wQ5`ToeP6fhCiOdNlU=G4<$oCkqKKK!6bV?^~WPHp)c`bk%e+CkbgcJd`%ud7AYUG$^Wlg-6cby)S`m1 z{}3Gv5qlAVioG~z`TJIdWT-nh@48!m2DeSTGNdc!(?_x#&@x1BcX_vi^3 zk&tZTNfGv6Pby18ZZHy+>AO!i?|nW-9-Fs@S&ufZTw|Z8(9&N?m~oj28rg`hEwX$6 zpD|lq2LdFt>`VSNOG$Z9710H##)TmL{mDMRQ@6X%Y>4A<{o2yqXDpB22mgL-luQ6r zdcK-C9`aKrC>HM6XGmBa?n*;e36bBG@LA@An?EI=H3wuTa3Y=s{q@G(q!1mvU||E# zUj4?W*j=2p<;4|?)DI`a3)-fr1kSgbR4worxk!KAwVeaJ9PbQmXUNaTBFRYQ6%hv` zxD9#D=80(=q%fM@$un0fP~}kRAJJ6-J_REahitbTepDtyEr3l^-;FN4{C0Q6d*!CN}~mkqMw zgIJqqNv24gebvx&TX?p*&5bjL-^H!R>6v9GkB~><{g(b!AbqE43WeW-h$Df;!On7? z0(sz-M7ONgZpekbF--Nh(poWrrA7TQDvR=$Yy`|vJmH%irmEUAsrJhC_eo(d zI5>Xmg*psO!o@0-^e=geg=;Gh)wO>Q<}c$yEe8bnzS>3e&kNTSzy;YUF8qI4^Y`e%QM&g= z{G6q~e;{Si3%+nd*7#4GW<`l!M)SQoe_kkL0~g5MYS}n`E6iUT*dI&x_X`4@A!i1H z&#|X?1i0h>G3G2|^h`#ITK?-o4%&1Lt>wJ=`!2tyAQI?fh||)mKQG*k2Ny`IiafV1 zgd9>eM@r2v*a$6MX0EjOk7oZpP*$F`zGWfL9~WtajdPUKZxm}@kcMQ#EJF{+$Ls9$ z$j1Wj{ds!<6_it>a=E&xe^Eut6J$r$|AW2gqMR<>^Q44;q=aypcJPa{@e{1O56>!r7v@>FYC;2b^JN)I0P)I=-nr}fEFkAi{oHBYL zJJM)r=hgb2?#3G3J}hH`cM6*l|6`6ZQ0?4E2t)NPv_E3;r#8XBJVPn}e{iV-o&*T2 zS3xeSL%Yl5w}h--f;Ao}st93dGP;DGr7RO{Q0$;tygFE*T1CZgaaYe^JrGm^v}o#> zlh=Z}h&9ktfx~^jw9*h{KZ*hJM96LBuIO6%VCQ(%WR!U&$d`>nE^ruu*m^A*)rZD^ zfehJ4vAZc{LK#AKx{DGw_CO?cZe*#R^-S!MbTgFYKv`@QeIyM#c&VR7JKnDlc^ z7RsYQW?+U6WG1c6wKhd8x+y_jc@jwLDeeJe%q2qCB3guQf1Rg`H^}=bbHeGoS9xne zZemcVsKOj%3g79w4d3Gmn*n(}o3-jW)Wz-%0Ku;W>C6f65k*NU9vRDL{ys5N_erNv zMU!Cmvb2h*r^INmI>3hN0r1G{N3~;sq|pMMAJBUUNUKxME~egaU$@<#IS=R&N*x2a zMKg5p|80-X?_DSzY=|(A+d#9rf2#G}=Sa%WDtg+n-cDq>vy3mOlm=j?=jAs z@sjNPcPfZ9k1yes07>_*s}{7|c_eE5L$jgQn4wK7dsF6)5wuKMNw3;MuL$vI%Otq}PpZ zG)^P>@YV8z`ix0pD!x|1;ZY&57%V&vthc?S&q0#AW}&FKGNrRT) zRF;u%ICFxd`4K4nu>%c7g#1npHr0~1sW+Mo_t{_9nNaAvs;^XnBYfK0oA*TaQy`^d z1LD7~+j(hv71ripu~mQE0lgV2&`ABYL?uMs@p`ROVu%?gx_C$wF#XTBF~LpnEq*!@ z)bhZdVW|GS7_VR}U{{{yfTF6)6CU$X`oh70(|ou>x*sEuSbQgT;%gXV^czL)`XTY~ z(Lj)dm;kvudvw~76CBg?n=gXkgwDR}eU$UtppU+q7@xzP(qJM9$$m{_Tb+Bz<9*BR;AzQs00 zFrj`q{8`h$gN1(m+vZ-ogHSapx-gg(Y%TG6IV*^$6fncJ*Q1(P;Q%sVISQErogXRz zMs5t9P?S77Fs*y7xX9Hwa{r8~;m7TxlQB{Tezp^sDTTnLSl>LEHMe+-JuY-mLvx=K}+oC8Bs1=6Fh#zk7N zg{NHOIIAX|YXY4sAZj)NAVA$rSY0?D?>sVbY3MWy;L0ym{@BEWf>oD^PSsMg&Xs)Y zqcz^T?<^I>dUi1HSPSM_pB-BlahKr>F52LtaNxkWW&Ojr-9Z&MvbiZ%Z*K@=d|R&0 zcpAzHAF@WiR!I|h@N}llmqib8SBL5(i>V28Qe8|r`QC%srVD*lHSMKN5%@S)3jae* zfETD9TbI|tgSv^UQ3SJQ`V4?>?c)cpz!YVn@eEv2PM}O}ER^1l^0i{`H)Znam3A5j zrIL~#Wzz#$TTEuz&@h(9*NgApvh0-M+!^|ek48)QMd~?NNhl!-CxV|6c!`GSG6cUK z+u>w#6{mjn1Arg8pRM+bB>nP$3)z8BhT^~r#t72+`KA#U*)Dw!p?LoGf`Q+md6m5K z+U~n%9#q!XW&jqXU;nbW%>*A3l-6g8hp8c6y(KGUakH!jAcOmg*v!-?lvv{uoT#iV zK>n&w`fZCOu&qHOFs-oXtl})mMz9r#` zA;G=W2*A?l@BaOnSbsH%Um9-uEqxLnXM_^so0qRJZvD9WJcHGpDm>=v0-Foy(!Y1S zu}DXOlb>FBe-nB~qyY}Ye|E$x*ey*$hh}1R2sbQH9@d!Bb6l_sjMhBl`ED2YGh}~O z$O>H+7Xh`a+B}PyGJff9us+~%qMz|?amY+SsyVLf(SGZJeSDA`CFJ~A0G=DT{_0~D zQtd)OgYU70-C{x!dpjCdG6rF=TLL-6!IHWk3c`L;)e_0beo9;go^Q$o4T|CPV(PbP znJLkov~>}|VckDZF9J!x=M2bzN(VIm^oEhL_+OO~!KR4I0M@VvFc$Y7pt4wJ8rY>^ z>Kv&4cWySqBT}G`khA#MzUs{GjgOmlSZ6*D@Sub&g(D;>CmQ~Z^%D1Rur~||k2<~R zhehw-GdGyJlmK?a`%{pemtDXR&NP6!O#nYQoph46ET)(6sn%-%sgYJJxtBDd?XIu_ zGl-PK(|$@4g=x@5hfc`(t9+uGnu(M{KtJV0Cb+cJ;!hup`VKI5S^qG0Elri$7lK{- z#7EKf0S0Y_P4^slV0wu7R3Ky&s4RR78oqZK_-x4v8xr(4DmVk+#0bFbq>6buSM&^$ z!hk6(SG=Q?cKs8&0gBeTwdkTYf+or?tBS|!RY>{vcb=7%ttKpDZBVHKh3*Zq`D=AEHAEbm?2VB@{HvXzci-u-0NEp|8ca~(GqT6jd z2p5(%&qzLJhF|QL1h5mXH(H|*W<<+v%(|yk3SkFMxGlSW2Kv_lIE6?!A@H4a8rYuv z?mbhL%n!VDjcOCUd+oNR`Bp*g3bmA1hM)Q=C?d*jMth??v$ie+s0R525NVO?G6FTb z`zwfGWd)XNGc7PiMm#@{YHCj)I;dg;)@lIFNey(b=Tjd9c6-*qz0gQ+u$u*bwiV<5 z`4NS|MYpR-v!oEo)15d#ZEXk`NlSs19j?F1Ky{zBN>veIm5PRe>@0@7Zafmiwo@X-A-4{0sls zQwVH*O^_5htlM)tBYF-s1?zJfN6EJl;6A)p3n2*|17=eQ(d`cuIsy<=W!m#aeg%AU|CvB zI;Z??#ZNngaB4-;^E_Fq7-`OKOxLUA-*Lv6M#a7j7nu~@%wjHll6Mn42I)8)5U@1l z@$Z{X6&2e7e7O|9c?%}CotChJ*~#}hG*7RCzu~>0nUl?{$T7*TFd)XOi@1 zfOFFa0w6Nyo?$|pJiKdX%I8R}JcoC;p{baeF-`sUOuSQjpL7G!EE>f$mU#BZmp+H= z`AUE9`2ek|p(3{HYCQ~-k!q2w;be}KDSIS&7&h>+WI!)%Cojl?lC01aBh5d*A@_U{~`k%F94fO&s(c7kqru z{Ze`jFLvmZ_jd=-=Dr;(7~%rHXg+be4oNxJ3Th!?QM*{%O>rS7e!&O zZli()M&tRb8qsD)2M*F+r@q|T{~bQ#ZRIteMN&s#*-A9Z@}occ~d&|YT-eFfo6i{;>fP<=*33xp8ryoba2dO>Wf<@{mo z2z|G?lgz1wwlkBdTN_zeNKfsj240>(Hh*wYc!*s~z zs2}xW{==z0Xq0*tNPm`Q5y!CG*_Aik5{^BAd~u)%>55nhvC?-4=Bc)Uq)EhylnD+KzhHxO}q5lR(w zMa0`EW)j*8iV z&!=mExcAzrsh8=3*oF3rYn^J9hmVbj2R|fx7wLtwJb`@MU0+=$EV`pby6{?)!1a3; z7tYhDs4UqhSY8G)Zn`$p$_VKu78+j>GD4kmrPJU&0&3 zWZu#d4zsu2^8-@xNA}5Gzcw_$ipNAaPV`+n+6Ro(el9|L`q@61_&Vs5T?cL;*(Un% z*?T)L0+Xc}$kE-qzA=x%>E#S3Xw~~ihhKp=1uHS@CNNLSkVDlu-F8AUBS|J#^c7@y zSK-tpya!?xl9l&L<>p;NPOny_^>>Qd0!J_Fd`~TPtx`GkqD__cPkX(U zv#(2znMxNo=*U}ofe8MR9CffDg&$$dmH~zP6NEe80fH%p6+R?}PWdiZgtT2mLf8)X zu}xs(_6BaP58bG~jKmi&Cl~CjnvkYq*JeJ~8XI%o{h zA0IbSd<6X4Dy?OMMKUnRix3#Abb`516rFr$`_l3~P9{=F#(Hl2nWXLFrTqGLee>^*Mgeiy+2u5K=?VP=fv$NseQi3dkUwh}B;j3M7q?FB?KfUFwDdC+ zpC*uH%X2cB>9E`5-gQp1m5yTeof%pNv5xXo!$Yq^m_CZj$?etc?&p;!J#Q`)oz-3L zMX%Kef;++o6{1zlkb&>=djnW}E~e+K4B_fYc8bo35>m$EOnAb17^A|)48-B^Of zV7KXYHa`EWd31}V#zG4q7kGQz-Z)JqG(pm|2RQjpEKjo6AxSykjAWZ}G^##11YOFG z(N_sBq8Z<#==U-RMIrFu99nvb`P()>#VGIo+ce54GUvwa2vp37){Nc@j@sguYFNKCT5- zO2Ig>C6*N)|F?#kLBPQKv`BCA2hU51iZo$M^ff^8tP)=S5wW)HWDXdiWl;7yZjxJA z`kVXvsg` zeVo~{W^ftJ)i{IotNKB?hmU%0_j1-cH_;1ROp`HFzb|=>_mYZp*(J^NS}dEOr)UJ9 zsQt0$xRh&Tzw-*;MEKgIi=JLyyprOZIp3w^s8HL$^0u-uBHD`OW1Ix$;Z*3=uDt_M zNX+-?8^LqWE(zZZP3FCpk4eQe`l2PCf`s#4+uKC4N}R4sht_uLZ1y@8lrrw|$7y+t z!7<~rxB-jc#E`BAG2NYht~25@_iP>~lT^CD7k0QuM5_T`8boum%QqE;5yT|3aj~yK z`Xow-4U4`V&~3U0vH7?!Ab&*Z%E@#pfL9CUMDL{l$=uE8bm8=B2&?Qw^bT{=7`l)T zS;m;P!JH?g^Up-g!RBQ^eq$$-*|OH+g=LJbhJ*e9zuZ*0gqd6Jx>?}2o0&t*=2_EN z-2n91K)E619%~ZC!|_z!(RBN-ia1&t!TQWE#fL`0UkGf)%x( zGJ`{DxNn6v_D`IKpNiM9SYgxD9{ADZiKqHqs)ET-L1noo;3Jp6;#DKnix1?#x}_BB ze>oEqBCUA5+hF>!3!zz2KCYsh%$$|VjfY)(O_rm>l!g%Hmv$4TB|%OgtV!R-7j}7q zB~Mt+H5N6>PP!bf-Rd$skDbdB3(T{lG>;TC-e%xlPtqH*`*_w;!Wwqdf)bpn%kNN22ca#7&`;v^o&W4h|?fI6H9k2znBa!$u zYFB_%QF~5Tzq=wPC^zUrGcEl_&4X3C5Od|&9IoM6N)=idul^bU6Hdlz^TN>w(dr7f z3p&I(4ABd8RuR#Nc0^E%nMw5I=(k#Btr9$a=v1hA=o?cRn+9stIFvy{VNYE=ORrI_ z-}2NX%^c*#orE*>TcE_4G(VL+qt!D}lWlbN5vol-X7LbY;L9i(d(TNN7DxRMEzP6% zDHzzO#=cwR1Q0@I`H)%iqjn<~{0sd!wkw#RBP=0|c}uPM)bQa`@dZbgJn;^ehKEA3{6qjKZ?ALfO6FK0p?pTd;nkDW~y4roK8FfLWmXn&0 zs`T{fWb96i+p~;YoD78XiuUZ*aaoN)gZFUay^dZu+w9%<$~)Z~vL97`sO{9({*)fI z*_Clx{+32@KMTX6H+5H{+efa;T?a)^AIsj%mJUlXUDqV$@(M_mi;1El z`5iCLLa-NCb)q&^iN~Mp=Xs&DM{iDZ;LK>6$h}%QrC^S!$424}&Koe!5}Fw}^W`_S zT`J8o^c&`UVjLxfQ@NanlvJL{^6?8CURC@f!rTjQ^uSzJ!qkK{7LY7LH@{K@ub8|4 zr;V)Woof%8&&ZsU1W-dQ}8J zRmtgfd{xotL(MbNm5-qa#U=O_Cm*PtDc!2}yj)L~e87 zNkOP`$m)Hj-G5w{806~6UpN|^qCmP@lL=66BlhyHiK{DReb=0lD?PpYBo7%L*_()( zslVMfF0mE2T2oZYR!a9l`$7vD^M$}#9`ML^_NkL>O-`;$XZrSQ5g2t*%;$uBKhI` z@-rTpo8u#DqpuKr9Pj_3BfLTv+ns9^X+DIH)AHbGobe>wX1UvCerCwmxscrHW0kLA zgIa8+eq;#koTe3RU&zrARft}`pvhR;%M2sDER~4D_{Ivy z)B~1AHorE(-<7Ox0DFCx-H;u{Gkh4qZg4w*8o=c87Zw2Vu@a^|@FmuIY32*}IZ|_8 z;QOQ@B{Ypn{j;R=($r6>41-@V&~HAyaK zR_^M&fSy_`Ir*gILK6%PJ*%H%4(|^BNEPbL6b)(l&!M-D(bnovSJC*=A5|zwF%{=x zYW{;9hsR*X6Qoh?TsHJEeo47hXmqHNaVlztD10{A0{zxL52aWC*mX}n)<5QYZIqJH z3B~s&23X2$D7$0wYx|QRe!GCnoaN`<=?Xz}W=80m|GFma2T2Koquzq(fqk95qY&|? z4vB1b`stdfc<=EEJhWI#>~Mi(iStYZtCygr7)LRuP%pZfZ)iX)pIwypn7ouG;CIU_ zml{)_703)8I-JhS$U?BsoK7SKO*;;gW6;8}2I37JVa#-^c=lZOZvks|AtcO^V4Us5 zse*c)xaS6f?aO9EjJgCyt-RGK0hi&SK*=4r!%`bQq(WZVTD76jT60%?O|JU=2f4F? zCDj3ES44D+=u~f{l=xBxF<%ZRe*fq}Nlb*I%PFJ8(M;ZbYt=Zr)Xb!fl$_?8=C#Dj z^+%AA+>2z5q<^wTi5r-qEB9jy>%TQLFVC_+7FrjNsj-H}3HkIPN&UIZ6(QT`_p{`5 z(V5M=stxdL;2f_~KS@#}GZ&>8-gdRb*gU)Zf#u|}XOrZ&+yPQlm<}wxbj_|B0{cYs zm{0pTl2VVJvS*x(02k{0J3dpJ;FqV=lY$GZEoBWa2l^i$GO3G)^Pv7S$wE)BEDX(2$ZtGs`2}Cipw&RAiVoDk$#9RBMth=I*`|f^tq5NBR?G z4mGkp!ix<#>KE|tWNGZOJ}rRL+YQB_;R1_Wf&21oOqdy!lWNNJxZL85nk6gjL^-r7 za{mmbv0?MaleB@JeXwz*}YEncqLd)X2fjq*L^%l91rKa;1F8F zKWc6=afQAGj@v!bdowjP#YJ;EN#~dqL{klAb&qQbOB{acw|nsVfH-AO`?C9GPNp1b zN13!K^jd(lWctm5Y_Zxb$70vw07Ape|LPQ$q}~$pS5!1P8c4sva<8+=Dt%e{<&tjy zVy9kk219`X4T?5XazYbmXYKu6CYDWYHT?A#f>X@*uZWMp@Y86}8?tzGjavqnf|=_# zmb(;#sS<35%L7g-7OBoCvoT`~4yQqzD=D$aOt%nXkgjqjptaIHx#!F(l=q)Ext= zLh;u}^t4&-4;BzRvw3cF3Ohy@1X&R?ZOmhYw&}eeUF_^k)gHmDh<_tA5tN+Mg{_x$ z>hm6qgr1MPLe4KMJ8awZvRvRnaI__hRak#ptu4mev@S`jO^jz5y#WOw-E}e;_4l{V;6yi>1XTc4Zzl z3A_m(ed#64QM<(i7cjYag2J?A2%F|_JC@yAPP6o!H8ehJm&A_rcp8>V+<8(jyJK1b z9+FYwrjii;RY*SZ`YJGH4e6D@y+Lv_~ z2A2MfN7MvHKaWjNzgM=8cu|r{?r-nmrXraif1VP3B47@3S%t;EZf}-%3u4=pwQ7gF zU$LDiNqM~FSzw+iY?&|Pugx5oH@cI_w?B?bUYIm8MUHO4c{n~{(>31%VU^DL8uo0X zTZ8eq(K|zD`!c$omQd-+Fm!UNH1iMe(YNNjRlo=~i2sxmuMK!@(;yNhg zV&Z6}6116~&r4ks;ggL8Iy)vis83EE-Dli4(Hq?yGg_ue8)u}slV1N>OODO?J!ZF9 z_{WUDM>tvShUoFhrDem{SI1dsZAAq(7DZ(mQpd8Kq?ZH6-4h}=MTQzaSN7wNhi<&m zJWskgU8x*KZb>F8ws(y5o;0o)|6m~AqIkJzvWukJT=U#Q%j0GRsj|R$ujcY-*_xS* zyWzoDJA+~rZGcq!A6JG-M(kw_Dh!9TvA16=9rW=XoW9iS?o{mBJ8O!tWsf2BKGZ*@ zR$#euh;i_f)yY?1Ffeq+xOiQ)$w>?vw0deR=a`rw%g=9+CMza-sgv`pWvst+9O2~l zCN-pPf)_&Yz@vW`ID*YIQ0cwouG@YFDBI-D(^ly5uKOcUV0F&>Gw+c{wt7&?-Q34f zuSqsPYDFmFDCCPo{9W_?hfGOD6AYKALjU);lzxJfDN?p1==kI74?g36@}T}^Kr(^* zI!(vs6PH)FqVVnNo|A-FtkTRYenDFV*#z}f!s*x7+IhqC^5^!5I0ecS-luE$#XYqA zek|_{@*@A{qr${YqkD~hWViopRIVOC(IP9rvby|#&S;!TJ^M+>AhY}bZUKOpZptO) z`#XX3AsUPc`y-x7-OcnT*Z1#1YQqERaxA#^94C|2z7Yf4eee1*iwPZR(@A^pT6PNZ2OMa zS?^7+)U-n(S^@`5L`5B=(PT#?D5?)>Xb zuN2fH>6SYODi&B|gHh69B)Pkyz*75rfRR7FyB9<_xJ?iuO1)RSTld9+g~QHU@)lQE zlt1HXc-P6a^ppAi&mal!#dR}=e>0d&NWCTA9lK?lt|g3LVLe!uL6NVv0V7<=Eyo_( z|2a9}r*%g5NcbLdZV_5~_55k+zK;`}Oa!S?HHV?1nEShqz82F>dRKS1)FW4(3E?aJ z^A%ABd@~WW*aw@73FcRQKfmX+v=Kc-fH{aqGV=ljO#z2cZ4qUeBvdp zKp|IKji|iYM~uw|$9m~hi*f%8?FXAUaIS~H%l}8Q!Gj1aF1hf~|A%uu45!PnCyG~o zm!9A*!3D#2`DwWS@f7ep#}EP1trz3(e!pOdu(a=17;$s|_7K6h5cPrE(v#n{C&71j zfRN;H`Q4CTB>zJ+azp+vPPrFaRo~_Pj8xMk4KjP6(q`m&%J>&q8D$U44*zalBl`!Z zg##}a3|=P0WG8TScz^oJPz@Fem@Y1qAZpq?P;n;q75}8ULap2Y=r8tza$3-10KPn` znmJZWIINl0S|XasD&!acKj{HHMdtN`6`VhJJCYjCl%`&Mdo03=aYzwSP3~uB|4FGw zg+fW);gH|?pDG-bJre|f>|htsA(Se?RL;u9>Ujl!Z(T?7=NFYg81b&VznVjA72j zq=GZ{=XVJe+ZpsjvQ_) zceP%hNRP^?PB~m*`O1>|uL&7U0RA3^FGt|#$_QrR#f0Esd{5ZaQIQ8K%fln6YYVJv z13)&hvd6&l=LcT~u|&f=e9`uwAEJAqPMjrH4Rb^#CUrR?TtwxO!?}WyzeP$&tuQWd zd&Ms?5lO91d`Cm*718Y_Y|LSpyT3*wDFvAqKIn>lKR?4!S6F8sxAvZiFwp7UPp193 zBeqE(N}-lMC<6Z&LWk2dP)4gQb!cw@3e9+0H~}1s2G+n;nP=cZ+^-dcNrXzd)O?K| zv;z6p0SoVpWCSy|+T%Ks>i_i$fBfmUsr;{PrW#otCNAfHs*V12;kRY;&;9Tw@B$WF z-Z%gD860>WLU|BM4K!0|_tf2g_CX2a;{v{?j$=AoD^1GZ zpIrao-lPC9rp_XMuh=K@9+51Bk_v4a{nuM=ehx|>oPk0D^#uJr!gLTFBEc31II=-@z9HK(@PPe-=QoWw= ziAY_XfnC4>+;whS7L! z9~2>7wZg(WvQh&ap+m?sMWE9UUy=)i1`&Nw>>B-=V0L*I@cyrrI+JdyfFJgEU@ez` zE72>vFXHkNyF3uG4lwIR`C2>25!yKLDzo~0wkXT!(!^}qgo+Np&z-;n#;CN~GhZud z+n&=sphLOXvAkD@V?|JDb(D(?yq~SZn}%~JW5RNRrFSA0etK5n*YH0w3$9|O6SR&p z)ZbEL)U-?~Reaka=w2VLfD**9Zx}JA88e1U+>lZOGkq=Y*bO-ICA8fWn@7<#3je2 zoMKUTT8n^G)IDK(yyy-mle@fUzFA?rKv~0v(?Dw$)R%v*ikK?@r$jcTRnX^_`x?Nm)pOM4?nYAv4o@a zv>O>=lt_)4c_-TEifm8I`VLr{2tNUe0DR4!I*R<~AX*mS`fJCLtG{JF zX3XPjA6vnc2bj2%?f}H_*0E?(P?q6gsHszP*~Wk`~Xm!2iaNcSDWq;(2ezp z2XCW=C$0{kw)jFtC8>cJSWQETZgNwNvYb0v3m^F>89txXT?+^G)D1wLFP2VvZQ@x~ zhk>0St7f~8`zxqxZal{O-n_sry8UAd;)>VQw{7EwO`LGaVNs4Wcjhl1Ii>2I$7*{Q z-+U-9y8-@x1Ae=1PM3&JYBj7ZD%`&J5mdjfRYlsS2&*j|jG{7nE6!Mm9!HGp8enl+ z_W%z`znqAj8@F_+7-NBk5hfR*L5!r@z|sCyud3(t4M1lyn0zAD1f&n%$|r6O%tmFi z%weEW$ZhcutOT#IaD=rmnM-%ZG~nxtE$c{_9n>T$FX2k|*d6P{7L}DI={3e=mI&$} zGfKjW(Vs?zy5j4ic;c~{2)g_ZZ?8BMh>gVc16`LV`{dY3Q0P{qEWSOjSn5|EjNcwu zrNr-3C#cLX(Jo^UBy>Ofq2C9)>h>7$H{Q(6Z2;S0lRFHZZ~Kwgd%v~fg95p+?|3Fa zXd;8ou%e>gKB}5R7*>aXLf7zc@0knHD(FG1bR=vogx&y0FR1^H20ef>C|F!oHo&yx zb{#1Fva*jVONTKPF|q#vS$apTI|Ki&?my({=o@WW*x zCwg}+snG!?=gQ~wAt?gV5=J@JJ z4y+W9uP!iiiW)}S%D>Sd7VrvezPw0N@1w3K{ScwsIgjUmI5~1pY7DcluJ9&wBVj47 z0xjJL)&dQFbfULPQVBMvfyo{YWx2P4a!|V?3O$J0(cP2>3tucs_1HPFKFsq%|y$5cltR70_^gh*Ya6`!M z?MESL9bpH|YhX?i1>Bqheg>`dy(5h)07d04xdXgJpLh-d*h~|8c z=|7my8hWts!#Mdh2nYp*nyepp!!Ti|qRijpb0gy=&7RpkQf5!3WB4)DK?;}{1r$wi zUGSFSkC_mxVZz9-O{0SS!V@NWjflQvVwUgjDxx>|>14bd~&yd$V zZ`Q>>DbyvH)ztX6OY@J+tu8)OI$jerJzeND*Y~zl$Oq`B_Y-b2hXFx!uhrCYhw!a{ zTCj}l%pZa%Nw8BccCDa}VPSskj>=N4pHUAACRr_Ah9gWz+bg&+rIqElg zb-0lWop;1V<3_+&W|1FTB$XHL$&Y8=yNNz0&vV;(Rs9T%eW$%3Q>f^BG8*_5HO>%Q;xRtRI4+PD!d= zw{J$%uWKD!AVZ_w`ss!sO#Yq?@&@o|XKo}jXzA<`-|&SqFseNNJuee~$)N+**{i@w zvy1O2N3}UIzA7G1Hra<7H_>KKoss{R5z~LfOTL{l*Te#K?R>e0##aCcke3ie?1+<5 z^#0W*>q^&eu;qig@#RW?&9=37k5NoqicWb&8vc18d@6TYQ6|&?!%ck8ZdH4>LgfRr z^BYQ8KjwaCaFtgO9aO+ql=u2dv-Qog-hy)#PyTU)VB*JLlyNZuFGRPy0?YXs6rk6Y z5sTKTN8|jm!I)$m7OBZRLLhxYpVY+a?E7Vt*{e2tarImso zF7YxX+d|3u-tt?EasN*%qdtm1_H`V)2pO`a2_d2gi?Xfi&xKybzCst`YX8|{uY?N6 zCbNZ@#LJvWx(3iFekX9_x38x~%91oTi4_E&>8VYML_Df(H?MzNmMnwP=k1L129_PT zNI?Zpx4;IyjaPm*pmkVd)12kLg(d1OUWP?4lW5g-2dZiyTxHLIx#`QSHgnz@Y8xQ< z-P@YXy-K4p*7bH`C3jN{D0f8prevNtlbrl);1#yAhs`>02(%TmoEH#;i z1l3$GF-38SaYPqtbb9Z;_FUz->rj%)a;wGdCY=vN7!BtAUKjs}yx5gN^Tnsvr{=lW(C(y^$pp)&l6iDAZ_t zBW7tdE|0-^(RRX*KOf62ur9V#6H%$G@yOvM?5rcU>x*mKxR@L>r-2tDjCYu4 z70@W%U{AGLo*t^j8M6g!C5drtxI2y*6ecVX(`M-!#3wgbv{+V(*gwA3nkjtKsW=B3 zFhY~vB{2}DA{G?izO+7Vor#Mcw9pi`uCYttpG>#A#d*GFQO#L|BerC&aHX^2Rj{1b z2YuM)&-y36Dt8Sdww&H7_$%regay5Ktx4`6Z{J1rALC2PVn0?!r+TuXHstVq^k=ea zHeCZ~F?TV#DLrImoS$#fJCwHwi!zjzvumFUxzp9M@bml#?ZojQ{>Syx#8M}da?pN8 zbNi*Ez`KG4Og8;XudmC|tx#UkHNavsZCe{n1x~PpQtD7)0rUVk=&9%!U)asd@^100 z%t`;cyhZ-TBW4d~0&c_{D*95sor|e=3aIr^g5ClG^3?18nRUc5Ivko*5avsq`%bWb zJ$~Y~$6Fb`RO@9!8LpC4p=n${T?nD-#!c7)Y!Zea%g5f+Cvyg}tu5@)iuU~WQFJq% zmm6Xq?~N18aoV&uZ>=A(R<3Pzdz}c&*g@;Z+NRq)`uEA!)?rD1T1#Yf^(@iV+61<* z1@qv_mIa{x^8(;3lpwv$v?E0e+$JSm0tzTGp{uK8gSsqXOy7z!Kfb+{Y+x&St(LMh zTS2KY&ul#N(z$ozYF4tb^AJP}Wl{0j0-G1SDA_K%loF$j_Q*i3R2g%rSO;S0En7(k zi%(+ZK+e%3taS4a%X<}KO?YZ+8QK?`1)OtbxrSS%DgzBPDwx4>HaW82cj0oFn{PHYM}OcJ#VDF5AaKhUmabu6EyVhsy8PlQ%po)1DhpIbs2p(W zrD{xtit<7y`b>gS;~ij8@e8RGIxLKcOSsPf#gVT>cdlp6%vA;UMy$T3y+O440J>AF ze%c_0u3={Q6)O^74?}?HMI;YZ4M$BXzX0yT8IlLmR#B=DyRbgnJnaHe>yM--QQ@In z@S(mT#wev6bi2v{Qm!eMUi&8kgbzl=3Lm0K9#=;{nFQsaI*~ z;}2+zB9p%I$(-%{QXg;bD2b?5WE{Yq+P3d{7FE*qZ@8i8MI2=vYPIc#m_xBz@X-4s%g({pRs5Bf} zd=i{~fN>6of+vhZy+JF|I4%SlKlRYP<_o-GH+CH@73c7pevW^9*1ryXDS8lfNINQQ z_Q+pQ3+^(2XO3T+27e-S87u;zaPi(;_A>c9SjQ8O4@^h zdG9CNAkNzAwp5)Pbpq7Cu)0eys}Iv3{T=KRehA?8UhP|B|GB^cPjN@`(eHH~s^9}~ zC$l35ekkVtfjQy*ngVR1Q~$$=;oqPolO5prZ=x^C|AdSG^+ApBe1ACw{G4Y$?T)`@GuMx{Y26K{TXI(r(!$PpKeY6zEA|G^P`Ym{}afBSMmlB`G=yf zJpOax4&bGY-0%KfTl#T7R6N}2z^?e;5zZhK7*Orav|&KrGftM?yxheZLgy0D*1Q5A zRFj0PFFp4SabV$aV*JVVUXW*4r*nRQ^s)>TQ5IX9lu4+I=eHN}zx@CfZ{>*fH?%FV z=MvMw>lGo=F$=pt)DewPF`|UgXu9+ZSXYHnc*-yjZxbcRng?`iFtJbg1oyRtg_Aw< zqN{ZnGStf!;A?&qok{Lol*0K!Dm{sK5vYVR*t=L0rFjuq*|nlSzaxDCrVR2+K5BV` z+j=9K9f~`psbO+=3F1Q6l#rNZR zK#-~`GY|&LsrBXBEoa2j0mLd^*?opB4bW8f(FMeNql4160H9eP+)dG-ehcI$E|AZb zR~CX6?GD?{Lv^uL26JDb8AA!O`t=-j@*Pa4>i| zU$M+8SM}wtbbr)wwv<=4-JVwHIk~=TFIEl>5WHT1;^+goL+(Qirz{Gb_MCLHsnXTG z>?j?KJ8}S2OH0t*A(3Pvil(eBzM$tdtpkq&^8{0Jf5i(yFfJ~GL!q~za?0F0Z5F`G zqJy<74Qm&A#V?z|P)!2+#&u@Y@-JnVl(8@boP4Hl*0=;ku3L619cW!@WY|qYb>!3P z(L9}j8}O!y<8|kjFN5N&e^FgC+>yv9%F$1dc$8gMphWhMMM+cv%2@H1Ly<@JG^lOD z!9IhC-+-USDKGlwkEJ*P0O?KK>2(wz4(a!EZnOg67LJq% zpN~8^Wo(u*egO$iE&`xiSe#mOeXY8qZK(S6*p(fHZ@i(;v9r(S3UB4^$4auEe z!z;zbgyU=EkRu})DT1+vAy^bVs4}rLpz+QQUivC;#2fS}?&asgt0VX2$^^Z8>LGHG zUnk|(d&7i!loAUwnY;v<)ywrT}v56RhiO1If5uR)woPmCRy&K3LY z(P6NNV7rS^L@$SadR?URaz0?U-DS=eM4bzi@z#?g+sb9h2an4TJBA_M=q@0Tq=`}Ur>+scUUeeMun)FT=w$Cn6Tv|h8hC~mnSpUS3!tKFy>z~LqFNxvqeR&(eQbE z*!C6{*6ftk7TmymGBfYy4*I026hy38;DT>m2Qk^v4qF{HX1bVK(hvYc-_X-a+Mk^E zJEzC(|bU2A<-w=1ggdc77P=W8AoneCYKEb+?0E8XLINo zVG*$tk3Hm;WuT3NSaGmZeH>IdFJ?!Y9+R?Nn}xYb#q=WCKE2hF5$f!1L?eC)MYc_Z z_2Go<4`-Vhp$kUS1-{5jLkA~gz-y==HoHvoyl;)rpEZnYo22Emfa!_ejf^2I{J-0k zx1pQHeB|m6!ji<^hjL5e!5Et8E@Wfb6MP!@)bb=^zDN*5gh7a~U)sdlXR`8m(n9Jb z`nxRmcqAmx^|-xa%ZJt-1=cUOhjG_YKtPMwQB(wzJGm__SwSDH&396Hrj9nVPSTHt z^dLKv^6D1b_3<~L1$(z5jiwm}yxN`e!YC8L8@N4l&9-2nz3%KY2_N*9WlWh3n3iPv?4?8#7?2?*y^Xd9&=ib!r z!Xf8wXpwyiz~z1=#HXmQu)h`z1$@5)rt8SR{fy=|`bGXLs^GLvoUEMBqC5kIYnnToVK+c+e|ly8kx#1Syj+Y}d_8&G4^p_D ztYC*wfGC(Na{6vZ-&>oKOiUx>G{%wAI5}g)bUh-}Yrli#P6-^ty#qx3p@VU!atpp5 z*0u5(oIYDLU4P;F`>}g*bpDWajTlT=p~0>L0NFf<@qR=*&!Cpi;h7=zez+kuJ=Cs% zwcJ*zk7M^WNR>4N-*klU<-b<=@Y+Xz*w47r#N%eCdndj*4U=*UWYw@eBlda^NlB7t zRqUz`thM^@^`qInmFJiF^_w1*9->M7GUk|BI6^*MBbzx%CfZ}aerw>MKQb%tIVj;E zsfK2PpQm}sK{;E?L2oj9B>5mqCrf)rn-o!i{uur7PfKGFDNp#)>J&HS{j|Olgw!Jb zxM3!5xihVsHeBMQw3&BkAy=GSZL3|6eO!mNNpH2oP)+qIS8jpVed^S{DgzvbF14=msb-ct=X!=x@&j7JFQ@jyxrMH}pVXJ@TRkMk5;D*_ym(f{X0H}MPnxj8AbJ3k zHX6EaMz-e`z58WR>I}C4!B$;nU%$r|MY}s+A{cWgWw6qDEkDZU3gvVN0 z;}Q+%FVic|#|=r73b;gl`;WwdFxM)QY>+Z-Vs81~iQnq+*Li>C4KMEXJx_e5e5ZVF zD+k0ra$D3SjwH;=J8kTKhyAK#!02ej*m8&R0&VKZ6twk-Mpd>PC0b zssx*YCpv3;$52R!Bc;j8>uh~kh$Q{Zi;p%Z7hQ<{_UV^MkB#3TwPeOQuyzIJVe?DmUXcy2LK|EC49G8WwAm z)^J_o9R4=s@~!|q?Wh}&!gG{x$`lvdRuqeAD;`~h2J9>QHlO$c%%Mc${qiAq6=@42Dv?&M z+*o@i_91ia<4u7-fd=e#yf9n2X6xNN<;}bz#zTeq5!BB57ad|F>lCO?JUi`w4Qi6IR1b!(kMU4mPU zx+=ECvg!kSzHDtcC2t-TI{vA!*s{}3UMWS{W#wCFda&Vy|r@GZ1*?51#{cZ)PaF4I~2Y#ddfte zIjm}EyC|FCo{G21{PStJX&=+CiHXEzWm3qOmVC6}s{~13Y_BnqFQxnmT>}8a7Hx{{ z@@Xb{yq(1wzO?S|e{@az+ViLE$0ijX!hFnfaa_u$Q|T-m zpbQ#z*i)|b61P*XoFo|tZRL&WSzqDP5wQ>!;bLOpTo77usG=hZqR>&Vls5&g1_xOu zEPe~EFF_q4%WQ)|WTA@X#zclb&kL{`Yd=9^e~P*eeT)M}7e&FGd4P*!@pR9o3^ zJ$29v=l!F6y%h2GBvWl(`W6!=;xLZ=9l=W&>Q>Z2fpZh&yCGbnJR7Z!O9fv$YQELb zoK$2pI;00;#LHDIo(qO;Lg-YM^9w@Q+5<8iGvs0=GF+NhpDrqlW@%R!mSpw>hzZf6VL-xhPjN7|M2I8XE20( zG}E8UyWAHo+e~RMHfS}o%5+Tc6#Z6$9sIH9+mb-H^>sC1l(J_c;G0kI>xA}Y>5aj# zal4uu$$v5m0hWfl*Q2~U_g-k@R-N8zv0_pEErm8uau6tun->9FG8scJh|w>f(nk-( z9>5$b%vx$EH(dxEfPzXl!wTK)sIu*OL4bf+hVQ<3L0p#S<6o5qReCjxFyzXdDh<{#{uy$ zG>e;T08C@Dz;d(rVZ^l!d1Ug8)*$gnmI!$Q=` z&GprZIk6_+y1c)28P)VrG1BUy#T~1&-=+$cQ#qCSS-*gF)cY!QfQ8cIsoMAb7E>}_ z!hwPk`g~sp$u6%tdW>6cC;US?T7KTB$T9UP<|8E)oX* zBgrAEg6Mo6?$|Y)7_#!h2K_mzwOW(UMljLaT(lm|L*Yy*U4dVXrh2CuKDTD;t;8Lg zIiXcH@t!uII>~Mwr&piBkZC}*n>`KhY@btSiSDM<3}?}(CW@SWvD~*ZwUfWX>g|Gv zIoge9Th2AD*CG}-@TGScUK><2w6Ax)v!aZ9DN*!L2^&7g$yU($>eX?TjDSWoGvg34~vq{XVwVQb=eoO?3T!E&Tr+5u}R#=-sBu>>2q-H=`MxLpTOta?xb zZ2r|IR0TH$YQj~<;FxW@L6xJt_JAaywHCH~!RyRhZ1K4!-Su=rM!C?+h=WkvLI2#K zgE3QCr1&yDBCX<6^q!+KV8!K`oWt5Hrz0V`H_LvldL(L$F|{A;XusH-CycMh8JWyK zTARO}3W?ma8ph2VYl68>Y%vNsa#~V&m$k_ioxC2Dw7%#Ug;Id+d0~cNmagnYhe}6@Oyi+=x!^OhUiuyrHLtJmc;e3-9_Ffim zq+$vruXQJvC3DKM?D^RJ{`(R6oNIqBnN&20*9BM%kS`HO%eB|vpKX=9Z=TR%>%0>6ixa&3J+Zmshr zk7Ib+d|dB|N&ja{v*cGIPpyrou0Rx7WE+1(w@(h{BKhV+?KzS;eC_who=Zzln=aC7ha-}(1K9XXp{uLG0cQ|N*kuZcCz-@8!1n`| z4)z1Dd(8Cc6OJE${pYvf7NiG)FIdX_j<;f_5k_h96Rux;^}oShj4xb}noiOC%L)2# zY?_4l>sQBISN$FAl|sXXv4NvnzxR1SRRj2q_whJ?=kMG9BzvRA0L#W#-PDNq9h=7c z03f?~{sZ%WE+EYS;(S^q4*!mSFUkU&cd%K0@9!7l5TU``)GpEg(0mb}+0Ik);y+M~ zzaJ2Q=Q%`AFsPW`_#@%`*KeJbfLr^CkB9y1-+qAGh6v-jrSeMbZwei%|E~`UMw&V7 zS-g1*^myG35S&J}HPwIKH&rl!=Mh+!Im6NU95Arh)9pXCT?nMU8y1Le%4vBguKz`~ zAe;qJ@+f#t*g%Q+OtZ8^5~@w*m}JD?MuPv0ldd-I$;(;?ny270R|Z{&2`<}oBk$z? zry@h`WxWDSQfH{Mi5|WS3QQkp9qbI9Mo)n0Xf&fO?eFuR1HUa8jKwVGzrLRYpyq?o zXV73?exLLSVJ5JjbpO(Lfzf{de}2lLiU!QLJlGg+vF*MLXxdw&HXyADJ_A?F?ra0` zXr_vDpMEps2d8@J&oPV$-cEsDvK6dJ0G_9>iZt{T*?#-*@Noj$@5==!{iL!(;B9VJ z(M)TpGDR11!q|u5ejA>6`koLVgwIrHW!^nHd?q*X-xZ@DB_iAo=66c#-!u6{@LlzQ zfZN81>z3)IpB# Q75vgvKd)AxY7_E*04%?R4FCWD diff --git a/frontend/InitialSetup.md b/frontend/InitialSetup.md deleted file mode 100644 index a6b4d60..0000000 --- a/frontend/InitialSetup.md +++ /dev/null @@ -1,76 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) - -## React Compiler - -The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information. - -Note: This will impact Vite dev & build performances. -You can also try [the experimental native React Compiler support in plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md#rust-react-compiler) by using `compiler: true` in the plugin options instead of using the Babel plugin. - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(["dist"]), - { - files: ["**/*.{ts,tsx}"], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ["./tsconfig.node.json", "./tsconfig.app.json"], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]); -``` - -You can also install [eslint-plugin-react-x](https://npmx.dev/package/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://npmx.dev/package/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from "eslint-plugin-react-x"; -import reactDom from "eslint-plugin-react-dom"; - -export default defineConfig([ - globalIgnores(["dist"]), - { - files: ["**/*.{ts,tsx}"], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs["recommended-typescript"], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ["./tsconfig.node.json", "./tsconfig.app.json"], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]); -``` diff --git a/frontend/src/api/graphql.ts b/frontend/src/api/graphql.ts index 2e1192e..c6812be 100644 --- a/frontend/src/api/graphql.ts +++ b/frontend/src/api/graphql.ts @@ -36,4 +36,4 @@ export async function graphqlRequest( } return result.data; -} \ No newline at end of file +} diff --git a/frontend/src/api/graphqlUrl.ts b/frontend/src/api/graphqlUrl.ts index 1d91417..94e4352 100644 --- a/frontend/src/api/graphqlUrl.ts +++ b/frontend/src/api/graphqlUrl.ts @@ -3,4 +3,4 @@ const defaultGraphqlUrl = import.meta.env.DEV : "/graphql"; export const GRAPHQL_URL = - import.meta.env.VITE_GRAPHQL_URL || defaultGraphqlUrl; \ No newline at end of file + import.meta.env.VITE_GRAPHQL_URL || defaultGraphqlUrl; diff --git a/frontend/src/apollo/client.ts b/frontend/src/apollo/client.ts index 7fb7994..2feb94a 100644 --- a/frontend/src/apollo/client.ts +++ b/frontend/src/apollo/client.ts @@ -1,14 +1,9 @@ -import { - ApolloClient, - InMemoryCache, - HttpLink, -} from "@apollo/client"; +import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client"; import { GRAPHQL_URL } from "../api/graphqlUrl"; -export const apolloClient = - new ApolloClient({ - link: new HttpLink({ - uri: GRAPHQL_URL, - }), - cache: new InMemoryCache(), - }); \ No newline at end of file +export const apolloClient = new ApolloClient({ + link: new HttpLink({ + uri: GRAPHQL_URL, + }), + cache: new InMemoryCache(), +}); diff --git a/frontend/src/components/common/FeedBack/FeedbackCard.css b/frontend/src/components/common/FeedBack/FeedbackCard.css index 0c4332c..50c3a65 100644 --- a/frontend/src/components/common/FeedBack/FeedbackCard.css +++ b/frontend/src/components/common/FeedBack/FeedbackCard.css @@ -1,57 +1,56 @@ - .feedback-card { - min-height: 220px; - padding: 32px 20px; + min-height: 220px; + padding: 32px 20px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 12px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; - text-align: center; + text-align: center; - background: var(--surface); - border: 1px solid var(--border); - border-radius: 10px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; } .feedback-card > svg { - color: var(--accent); + color: var(--accent); } .feedback-card--error { - border-color: #78424c; + border-color: #78424c; } .feedback-card--error > svg { - color: #fb7185; + color: #fb7185; } .feedback-card h2 { - margin: 0; - color: var(--text-heading); - font-size: 20px; + margin: 0; + color: var(--text-heading); + font-size: 20px; } .feedback-card p { - margin: 0; - color: var(--text); - font-size: 14px; + margin: 0; + color: var(--text); + font-size: 14px; } .feedback-spinner { - animation: feedback-spin 1s linear infinite; + animation: feedback-spin 1s linear infinite; } @keyframes feedback-spin { - to { - transform: rotate(360deg); - } + to { + transform: rotate(360deg); + } } @media (prefers-reduced-motion: reduce) { - .feedback-spinner { - animation: none; - } + .feedback-spinner { + animation: none; + } } diff --git a/frontend/src/components/common/FeedBack/FeedbackCard.tsx b/frontend/src/components/common/FeedBack/FeedbackCard.tsx index 098f9f7..3771bdb 100644 --- a/frontend/src/components/common/FeedBack/FeedbackCard.tsx +++ b/frontend/src/components/common/FeedBack/FeedbackCard.tsx @@ -1,39 +1,37 @@ - import { AlertCircle, LoaderCircle, SearchX } from "lucide-react"; import "./FeedbackCard.css"; type FeedbackCardProps = { - variant: "loading" | "error" | "empty"; - title: string; - message?: string; + variant: "loading" | "error" | "empty"; + title: string; + message?: string; }; function FeedbackCard({ variant, title, message }: FeedbackCardProps) { - - const icons = { - loading: LoaderCircle, - error: AlertCircle, - empty: SearchX, - }; - - const Icon = icons[variant]; - - return ( -
-
- ); + const icons = { + loading: LoaderCircle, + error: AlertCircle, + empty: SearchX, + }; + + const Icon = icons[variant]; + + return ( +
+
+ ); } export default FeedbackCard; diff --git a/frontend/src/components/common/Layout/AppLayout.css b/frontend/src/components/common/Layout/AppLayout.css index b8da619..112d8a0 100644 --- a/frontend/src/components/common/Layout/AppLayout.css +++ b/frontend/src/components/common/Layout/AppLayout.css @@ -20,4 +20,4 @@ .page-content { padding: 20px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/common/Layout/AppLayout.tsx b/frontend/src/components/common/Layout/AppLayout.tsx index 6b6aa1f..8f6c7ef 100644 --- a/frontend/src/components/common/Layout/AppLayout.tsx +++ b/frontend/src/components/common/Layout/AppLayout.tsx @@ -4,12 +4,12 @@ import SideBar from "./sidebar/SideBar"; import "./AppLayout.css"; /** - * + * * Definerer felles layot som brukes av alle pages på nettsiden. - * - * Layoten består av en Header og en Sidebar. Det aktive page innholdet rendres - * inne i , bassert på hvilken Routte brukeren er på nå . - * + * + * Layoten består av en Header og en Sidebar. Det aktive page innholdet rendres + * inne i , bassert på hvilken Routte brukeren er på nå . + * * På denne måten kan Header og Sidebar gjenbrukes på tvers av alle sider uten at de må * importeres og rendres separat i hver page-komponent. */ @@ -29,4 +29,4 @@ function AppLayout() { ); } -export default AppLayout; \ No newline at end of file +export default AppLayout; diff --git a/frontend/src/components/common/Layout/header/Header.css b/frontend/src/components/common/Layout/header/Header.css index 6cba2ea..d6d1312 100644 --- a/frontend/src/components/common/Layout/header/Header.css +++ b/frontend/src/components/common/Layout/header/Header.css @@ -209,4 +209,4 @@ width: 19px; height: 19px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/common/Layout/header/Header.tsx b/frontend/src/components/common/Layout/header/Header.tsx index 48e16ec..3b7458e 100644 --- a/frontend/src/components/common/Layout/header/Header.tsx +++ b/frontend/src/components/common/Layout/header/Header.tsx @@ -1,13 +1,20 @@ import { NavLink } from "react-router-dom"; -import {Bookmark, Crosshair,Home, Settings,Shield,Users,Database, UserRound} from "lucide-react"; +import { + Bookmark, + Crosshair, + Home, + Settings, + Shield, + Users, + Database, + UserRound, +} from "lucide-react"; import "./Header.css"; - - /** * Header inneholder logoen CyberThreats, med NavLink for å sende til HomePage, * samt link til MITRE for å sjekke ut datakilden, samt en profil-knapp - * + * * Siden inneholder også en media-query for som viser sidebar navigasjon-icons ved mindre skjærmer */ function Header() { @@ -44,16 +51,23 @@ function Header() { ); } export default Header; - diff --git a/frontend/src/components/common/Layout/sidebar/SideBar.tsx b/frontend/src/components/common/Layout/sidebar/SideBar.tsx index e567f11..711b6f5 100644 --- a/frontend/src/components/common/Layout/sidebar/SideBar.tsx +++ b/frontend/src/components/common/Layout/sidebar/SideBar.tsx @@ -1,15 +1,22 @@ -import {Bookmark,Crosshair,Home,Settings,Shield,Users} from "lucide-react"; +import { + Bookmark, + Crosshair, + Home, + Settings, + Shield, + Users, +} from "lucide-react"; import { NavLink } from "react-router-dom"; import "./SideBar.css"; /** * Oppretter sidebar navigasjon på nettsiden - * + * * Tilbyr navigasjons lenker til ulike pages via React Router NavLink. - * + * * På mindre skjærmer er sidebar skjult gjennom media-query, og icons fra NavLinks * blir istedet vist i header.tsx componenten. Responsiv design - * + * */ function SideBar() { return ( @@ -49,4 +56,4 @@ function SideBar() { ); } -export default SideBar; \ No newline at end of file +export default SideBar; diff --git a/frontend/src/components/common/Navigation/BackButton.css b/frontend/src/components/common/Navigation/BackButton.css index 1d8a4c4..419ce3c 100644 --- a/frontend/src/components/common/Navigation/BackButton.css +++ b/frontend/src/components/common/Navigation/BackButton.css @@ -1,27 +1,26 @@ - .back-navigation { - display: inline-flex; - align-items: center; - gap: 6px; - width: fit-content; + display: inline-flex; + align-items: center; + gap: 6px; + width: fit-content; - padding: 0; - border: none; - background: none; + padding: 0; + border: none; + background: none; - color: var(--accent); - font: inherit; - font-size: 14px; - font-weight: 600; + color: var(--accent); + font: inherit; + font-size: 14px; + font-weight: 600; - cursor: pointer; + cursor: pointer; } .back-navigation:hover { - text-decoration: underline; + text-decoration: underline; } .back-navigation:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 4px; + outline: 2px solid var(--accent); + outline-offset: 4px; } diff --git a/frontend/src/components/common/Navigation/BackButton.tsx b/frontend/src/components/common/Navigation/BackButton.tsx index bd93832..9fb4524 100644 --- a/frontend/src/components/common/Navigation/BackButton.tsx +++ b/frontend/src/components/common/Navigation/BackButton.tsx @@ -1,10 +1,9 @@ - import { ArrowLeft } from "lucide-react"; import { useNavigate } from "react-router-dom"; import "./BackButton.css"; type BackButtonProps = { - fallbackTo: string; + fallbackTo: string; }; /** @@ -15,29 +14,30 @@ type BackButtonProps = { * @param fallbackTo - Standardsiden brukeren sendes til. */ function BackButton({ fallbackTo }: BackButtonProps) { - /** Innebygd React-Hook som lar oss navigere mellom sider - * Den "husker" den browse-historikken og dermed - * kan man bruke navigate(-1) for å gå tilbake. - */ - const navigate = useNavigate(); + /** Innebygd React-Hook som lar oss navigere mellom sider + * Den "husker" den browse-historikken og dermed + * kan man bruke navigate(-1) for å gå tilbake. + */ + const navigate = useNavigate(); - /** Håndterer trykking av tilbake knappen */ - const handleBack = () => { - /** Hvis de er noe å gå tilbake til */ - if (window.history.state?.idx > 0) { - /** Gå tilbkae */ - navigate(-1); - return; - } - /** Hvis de er ikk noe å gå tilbake til: gå til startpage */ - navigate(fallbackTo, { replace: true }); - }; + /** Håndterer trykking av tilbake knappen */ + const handleBack = () => { + /** Hvis de er noe å gå tilbake til */ + if (window.history.state?.idx > 0) { + /** Gå tilbkae */ + navigate(-1); + return; + } + /** Hvis de er ikk noe å gå tilbake til: gå til startpage */ + navigate(fallbackTo, { replace: true }); + }; - return ( - - ); + return ( + + ); } export default BackButton; diff --git a/frontend/src/components/common/PageHeader/PageHeader.css b/frontend/src/components/common/PageHeader/PageHeader.css index 2ddbc22..5e3a030 100644 --- a/frontend/src/components/common/PageHeader/PageHeader.css +++ b/frontend/src/components/common/PageHeader/PageHeader.css @@ -1,31 +1,30 @@ - /* ===== Felles Page Header ===== */ .page-header { - display: flex; - flex-direction: column; - gap: 8px; + display: flex; + flex-direction: column; + gap: 8px; } .page-header__eyebrow { - color: var(--text-muted); + color: var(--text-muted); - font-size: 13px; - font-weight: 700; - letter-spacing: 0.08em; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.08em; } .page-header__title { - font-size: clamp(28px, 3vw, 40px); - font-weight: 800; - line-height: 1; + font-size: clamp(28px, 3vw, 40px); + font-weight: 800; + line-height: 1; } .page-header__description { - max-width: 760px; + max-width: 760px; - color: var(--text); + color: var(--text); - font-size: 16px; - line-height: 1.5; + font-size: 16px; + line-height: 1.5; } diff --git a/frontend/src/components/common/PageHeader/PageHeader.tsx b/frontend/src/components/common/PageHeader/PageHeader.tsx index b6b7c63..0c23f53 100644 --- a/frontend/src/components/common/PageHeader/PageHeader.tsx +++ b/frontend/src/components/common/PageHeader/PageHeader.tsx @@ -2,9 +2,9 @@ import "./PageHeader.css"; type PageHeaderProps = { - title: string; - description: string; - children?: React.ReactNode; // brukes i software + title: string; + description: string; + children?: React.ReactNode; // brukes i software }; /** @@ -17,23 +17,17 @@ type PageHeaderProps = { * @param description - Kort beskrivelse av sidens innhold. * @param children - Valgfritt React-innhold som vises under beskrivelsen. */ -function PageHeader({title, description}: PageHeaderProps) { - const eyebrow = "THREAT INTELLIGENCE" - return ( -
- {eyebrow && ( - - {eyebrow} - - )} +function PageHeader({ title, description }: PageHeaderProps) { + const eyebrow = "THREAT INTELLIGENCE"; + return ( +
+ {eyebrow && {eyebrow}} -

{title}

+

{title}

-

- {description} -

-
- ); +

{description}

+
+ ); } -export default PageHeader; \ No newline at end of file +export default PageHeader; diff --git a/frontend/src/components/common/SearchBar/SearchBar.css b/frontend/src/components/common/SearchBar/SearchBar.css index 7709511..159196b 100644 --- a/frontend/src/components/common/SearchBar/SearchBar.css +++ b/frontend/src/components/common/SearchBar/SearchBar.css @@ -1,95 +1,94 @@ - /* ===== Felles SearchBar ===== */ .search-bar { - width: 100%; - min-height: 52px; + width: 100%; + min-height: 52px; - display: flex; - align-items: center; + display: flex; + align-items: center; - overflow: hidden; + overflow: hidden; - background: var(--surface); + background: var(--surface); - border: 1px solid var(--border); - border-radius: 8px; + border: 1px solid var(--border); + border-radius: 8px; - transition: - border-color 0.2s ease, - background-color 0.2s ease; + transition: + border-color 0.2s ease, + background-color 0.2s ease; } /* Marker søkefeltet når input har fokus */ .search-bar:focus-within { - border-color: var(--accent); + border-color: var(--accent); } /* Container for søkeikon og input */ .search-bar__field { - flex: 1; - min-width: 0; + flex: 1; + min-width: 0; - display: flex; - align-items: center; + display: flex; + align-items: center; } /* Søkeikon */ .search-bar__field > svg { - width: 22px; - height: 22px; + width: 22px; + height: 22px; - margin-left: 16px; + margin-left: 16px; - color: var(--text-muted); - flex-shrink: 0; + color: var(--text-muted); + flex-shrink: 0; } .search-bar:focus-within .search-bar__field > svg { - color: var(--accent); + color: var(--accent); } /* Inputfelt */ .search-bar__field input { - flex: 1; - min-width: 0; + flex: 1; + min-width: 0; - padding: 14px 16px; + padding: 14px 16px; - border: none; - outline: none; + border: none; + outline: none; - background: transparent; - color: #f0f7f7; - font-size: 14px; + background: transparent; + color: #f0f7f7; + font-size: 14px; } /* Search-knapp */ .search-bar__button { - align-self: stretch; + align-self: stretch; - min-width: 120px; - padding: 0 24px; + min-width: 120px; + padding: 0 24px; - border: none; + border: none; - background: var(--accent); - color: var(--bg); + background: var(--accent); + color: var(--bg); - font-weight: 700; - cursor: pointer; + font-weight: 700; + cursor: pointer; } /* Tastaturnavigasjon */ .search-bar__button:focus-visible { - outline: 2px solid var(--text); - outline-offset: -4px; + outline: 2px solid var(--text); + outline-offset: -4px; } /* Tilpasning for mindre skjermer */ @media (max-width: 480px) { - .search-bar__button { - min-width: 90px; - padding: 0 12px; - } + .search-bar__button { + min-width: 90px; + padding: 0 12px; + } } diff --git a/frontend/src/components/common/SearchBar/SearchBar.tsx b/frontend/src/components/common/SearchBar/SearchBar.tsx index f6fb5ba..2d5e709 100644 --- a/frontend/src/components/common/SearchBar/SearchBar.tsx +++ b/frontend/src/components/common/SearchBar/SearchBar.tsx @@ -1,12 +1,11 @@ - import { Search } from "lucide-react"; import "./SearchBar.css"; type SearchBarProps = { - value: string; - placeholder: string; - onChange: (value: string) => void; - onSearch?: (value: string) => void; + value: string; + placeholder: string; + onChange: (value: string) => void; + onSearch?: (value: string) => void; }; /** @@ -20,33 +19,33 @@ type SearchBarProps = { * @param onChange - Kalles når søketeksten endres. * @param onSearch - Valgfri callback når søket sendes inn. */ -function SearchBar({value, placeholder,onChange, onSearch}: SearchBarProps) { - return ( - { - event.preventDefault(); - onSearch?.(value); - }} - > -
-
- - - ); + + + ); } export default SearchBar; diff --git a/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css b/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css index 6cffe66..863e831 100644 --- a/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css +++ b/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.css @@ -9,7 +9,6 @@ border-radius: 8px; } - /* ===== Header ===== */ .related-techniques-header { @@ -43,7 +42,6 @@ line-height: 1.4; } - /* ===== Search ===== */ .related-techniques-search { @@ -115,7 +113,6 @@ opacity: 0.9; } - /* ===== List ===== */ .related-techniques-list { @@ -173,7 +170,6 @@ outline-offset: 2px; } - /* ===== Technique row ===== */ .related-technique-row { @@ -199,7 +195,6 @@ background: var(--surface-hover); } - /* ===== Icon ===== */ .related-technique-icon { @@ -224,7 +219,6 @@ height: 18px; } - /* ===== MITRE ID ===== */ .related-technique-id { @@ -246,7 +240,6 @@ grid-row: 1; } - /* ===== Technique text ===== */ .related-technique-content { @@ -262,7 +255,6 @@ text-align: center; } - /* ===== Arrow ===== */ .related-technique-arrow { @@ -279,7 +271,6 @@ color: var(--accent); } - /* ===== Tablet / mobile ===== */ @media (max-width: 700px) { @@ -310,4 +301,4 @@ padding: 4px 6px; font-size: 12px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.tsx b/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.tsx index 9464fe0..f704543 100644 --- a/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.tsx +++ b/frontend/src/components/tactic/TacticDetailed/RelatedTechniques/RelatedTechniques.tsx @@ -38,37 +38,25 @@ function RelatedTechniques({
-

- Related Techniques ({totalCount}) -

+

Related Techniques ({totalCount})

-

- Techniques that can be used to achieve {tacticName}. -

+

Techniques that can be used to achieve {tacticName}.

- +
- +
{techniques.map((technique) => ( -
+
@@ -115,4 +103,4 @@ function RelatedTechniques({ ); } -export default RelatedTechniques; \ No newline at end of file +export default RelatedTechniques; diff --git a/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.css b/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.css index 69c7e4e..193c775 100644 --- a/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.css +++ b/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.css @@ -35,4 +35,4 @@ .tactic-description p { font-size: 12px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.tsx b/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.tsx index 2a6cfc9..959fb65 100644 --- a/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.tsx +++ b/frontend/src/components/tactic/TacticDetailed/TacticDescription/TacticDescription.tsx @@ -4,9 +4,7 @@ type TacticDescriptionProps = { description: string; }; -function TacticDescription({ - description, -}: TacticDescriptionProps) { +function TacticDescription({ description }: TacticDescriptionProps) { return (

Description

@@ -15,4 +13,4 @@ function TacticDescription({ ); } -export default TacticDescription; \ No newline at end of file +export default TacticDescription; diff --git a/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.css b/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.css index 25c21f8..bb17697 100644 --- a/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.css +++ b/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.css @@ -6,7 +6,6 @@ gap: 8px; } - /* ===== Back link ===== */ .tactic-detailed-back-link { @@ -32,7 +31,6 @@ text-decoration: underline; } - /* ===== Type ===== */ .tactic-detailed-type { @@ -45,7 +43,6 @@ letter-spacing: 0.08em; } - /* ===== Title ===== */ .tactic-detailed-title-row { @@ -68,7 +65,6 @@ line-height: 1; } - /* MITRE ID badge */ .tactic-detailed-title-row p { @@ -86,7 +82,6 @@ font-weight: 700; } - /* ===== Favorite ===== */ .tactic-detailed-favorite-button { @@ -116,7 +111,6 @@ color: var(--accent); } - /* ===== Responsive ===== */ @media (max-width: 520px) { @@ -132,4 +126,4 @@ width: 32px; height: 32px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx b/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx index 3e92050..447da79 100644 --- a/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx +++ b/frontend/src/components/tactic/TacticDetailed/TacticDetailedHeader/TacticDetailedHeader.tsx @@ -8,17 +8,12 @@ type TacticDetailedHeaderProps = { name: string; }; -function TacticDetailedHeader({ - mitreId, - name, -}: TacticDetailedHeaderProps) { +function TacticDetailedHeader({ mitreId, name }: TacticDetailedHeaderProps) { return (
- - TACTIC - + TACTIC
@@ -38,4 +33,4 @@ function TacticDetailedHeader({ ); } -export default TacticDetailedHeader; \ No newline at end of file +export default TacticDetailedHeader; diff --git a/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.css b/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.css index 64c3269..813cd27 100644 --- a/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.css +++ b/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.css @@ -16,7 +16,6 @@ border-radius: 8px; } - /* ===== Header ===== */ .tactic-card-header { @@ -57,7 +56,6 @@ font-weight: 700; } - /* ===== Content ===== */ .tactic-card-content { @@ -78,7 +76,6 @@ line-height: 1.4; } - /* ===== Footer ===== */ .tactic-card-footer { @@ -102,7 +99,6 @@ flex-shrink: 0; } - /* ===== Hover ===== */ .tactic-card:hover { @@ -113,7 +109,6 @@ text-decoration: underline; } - /* ===== Individual tactic colors ===== */ .tactic-initial-access .tactic-card-icon { @@ -124,7 +119,6 @@ background: rgba(43, 179, 255, 0.12); } - .tactic-execution .tactic-card-icon { color: #3df0b3; } @@ -133,7 +127,6 @@ background: rgba(61, 240, 179, 0.12); } - .tactic-persistence .tactic-card-icon { color: #b56cff; } @@ -142,7 +135,6 @@ background: rgba(181, 108, 255, 0.12); } - .tactic-privilege-escalation .tactic-card-icon { color: #ff5d73; } @@ -151,7 +143,6 @@ background: rgba(255, 93, 115, 0.12); } - .tactic-defense-evasion .tactic-card-icon { color: #e8eeee; } @@ -160,7 +151,6 @@ background: rgba(232, 238, 238, 0.1); } - .tactic-credential-access .tactic-card-icon { color: #ffd84a; } @@ -169,7 +159,6 @@ background: rgba(255, 216, 74, 0.12); } - .tactic-discovery .tactic-card-icon { color: #26e4ff; } @@ -178,7 +167,6 @@ background: rgba(38, 228, 255, 0.12); } - .tactic-lateral-movement .tactic-card-icon { color: #ff5ccf; } @@ -187,7 +175,6 @@ background: rgba(255, 92, 207, 0.12); } - .tactic-collection .tactic-card-icon { color: #ffad33; } @@ -196,7 +183,6 @@ background: rgba(255, 173, 51, 0.12); } - .tactic-exfiltration .tactic-card-icon { color: #35f0d2; } @@ -205,7 +191,6 @@ background: rgba(53, 240, 210, 0.12); } - .tactic-command-control .tactic-card-icon { color: #b96cff; } @@ -214,7 +199,6 @@ background: rgba(185, 108, 255, 0.12); } - .tactic-impact .tactic-card-icon { color: #ff5d73; } @@ -223,7 +207,6 @@ background: rgba(255, 93, 115, 0.12); } - .tactic-resource-development .tactic-card-icon { color: #52d67a; } @@ -232,7 +215,6 @@ background: rgba(82, 214, 122, 0.12); } - .tactic-reconnaissance .tactic-card-icon { color: #56b6ff; } @@ -241,7 +223,6 @@ background: rgba(86, 182, 255, 0.12); } - .tactic-defense-impairment .tactic-card-icon { color: #ff9f43; } @@ -250,14 +231,12 @@ background: rgba(255, 159, 67, 0.12); } - /* ===== Fallback ===== */ .tactic-default .tactic-card-icon { color: var(--accent); } - /* ===== Mobile ===== */ @media (max-width: 520px) { @@ -279,4 +258,4 @@ .tactic-card-content h2 { font-size: 14px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx b/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx index 03687b9..3982861 100644 --- a/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx +++ b/frontend/src/components/tactic/TacticMain/TacticCard/TacticCard.tsx @@ -18,10 +18,7 @@ function TacticCard({ description, techniqueCount, }: TacticCardProps) { - const { - icon: Icon, - colorClass, - } = getTacticVisual(mitreId); + const { icon: Icon, colorClass } = getTacticVisual(mitreId); return (
@@ -30,9 +27,7 @@ function TacticCard({
- - {mitreId} - + {mitreId}
@@ -41,10 +36,7 @@ function TacticCard({

{description}

- + {techniqueCount} techniques @@ -53,4 +45,4 @@ function TacticCard({ ); } -export default TacticCard; \ No newline at end of file +export default TacticCard; diff --git a/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.css b/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.css index 2c52142..35155d0 100644 --- a/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.css +++ b/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.css @@ -8,7 +8,6 @@ gap: 12px 16px; } - /* ===== Responsive ===== */ @media (max-width: 1200px) { @@ -27,4 +26,4 @@ .tactic-grid { grid-template-columns: 1fr; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.tsx b/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.tsx index 2d1a31f..0ac5c5c 100644 --- a/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.tsx +++ b/frontend/src/components/tactic/TacticMain/TacticGrid/TacticGrid.tsx @@ -28,4 +28,4 @@ function TacticGrid({ tactics }: TacticGridProps) { ); } -export default TacticGrid; \ No newline at end of file +export default TacticGrid; diff --git a/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.css b/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.css index e2126fe..7dae615 100644 --- a/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.css +++ b/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.css @@ -6,7 +6,6 @@ gap: 20px; } - /* ===== Header ===== */ .tactic-toolbar-header { @@ -38,7 +37,6 @@ line-height: 1.5; } - /* ===== Search ===== */ .tactic-toolbar-search { @@ -105,7 +103,6 @@ opacity: 0.9; } - /* ===== Controls ===== */ .tactic-toolbar-controls { @@ -177,7 +174,6 @@ border-color: var(--accent); } - /* ===== Tablet ===== */ @media (max-width: 768px) { @@ -194,7 +190,6 @@ } } - /* ===== Mobile ===== */ @media (max-width: 520px) { @@ -213,4 +208,4 @@ .tactic-search-button { padding: 13px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx b/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx index 92ab8b2..8499bf3 100644 --- a/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx +++ b/frontend/src/components/tactic/TacticMain/TacticToolbar/TacticToolbar.tsx @@ -26,21 +26,18 @@ function TacticToolbar() {
-
- - Sort by - + Sort by - +

); } -export default TacticToolbar; \ No newline at end of file +export default TacticToolbar; diff --git a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.css b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.css index 39035ef..923b191 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.css +++ b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.css @@ -67,8 +67,8 @@ } .technique-card-content h2 { -font-size: 18px; -font-weight: 700; + font-size: 18px; + font-weight: 700; } .technique-card-content p { @@ -101,13 +101,12 @@ font-weight: 700; } .technique-tactic-tag { -background: rgba(181, 108, 255, 0.18); -color: #d58cff; + background: rgba(181, 108, 255, 0.18); + color: #d58cff; } -  -.technique-platform-tag { -background: rgba(59, 130, 246, 0.18); -color: #a5d4ff; +  .technique-platform-tag { + background: rgba(59, 130, 246, 0.18); + color: #a5d4ff; } /* ===== Footer ===== */ @@ -233,4 +232,4 @@ color: #a5d4ff; .technique-credential-access .technique-tactic-tag { background: rgba(86, 182, 255, 0.18); color: #8fd3ff; -} \ No newline at end of file +} diff --git a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx index 2aa9514..e7d172f 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueCard/TechniqueCard.tsx @@ -16,47 +16,40 @@ function TechniqueCard({ tactic, platform, }: TechniqueCardProps) { - console.log("TACTIC:", tactic); - const { - icon: TechniqueIcon, - colorClass, - } = getTechniqueCardVisual(tactic); - return ( -
-
-
- -
- -
- - {mitreId} - -

- {name} -

-
-
+ console.log("TACTIC:", tactic); + const { icon: TechniqueIcon, colorClass } = getTechniqueCardVisual(tactic); + return ( +
+
+
+ +
-
- - {tactic ?? "Unknown"} - +
+ {mitreId} +

{name}

+
+
- - {platform ?? "Unknown"} - -
+
+ + {tactic ?? "Unknown"} + - - Open profile - - - - ); + + {platform ?? "Unknown"} + +
+ + + Open profile + + + + ); } -export default TechniqueCard; \ No newline at end of file +export default TechniqueCard; diff --git a/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx b/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx index 763b1b3..18babc2 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueCard/getTechniqueCardVisual.tsx @@ -10,7 +10,7 @@ import { FileText, KeyRound, Upload, - AlertTriangle + AlertTriangle, } from "lucide-react"; import type { LucideIcon } from "lucide-react"; @@ -26,12 +26,12 @@ const tacticVisualMap: Record = { colorClass: "technique-initial-access", }, - "Execution": { + Execution: { icon: TerminalSquare, colorClass: "technique-execution", }, - "Persistence": { + Persistence: { icon: Layers3, colorClass: "technique-persistence", }, @@ -51,7 +51,7 @@ const tacticVisualMap: Record = { colorClass: "technique-credential-access", }, - "Discovery": { + Discovery: { icon: Eye, colorClass: "technique-discovery", }, @@ -61,7 +61,7 @@ const tacticVisualMap: Record = { colorClass: "technique-lateral-movement", }, - "Collection": { + Collection: { icon: FileText, colorClass: "technique-collection", }, @@ -71,20 +71,18 @@ const tacticVisualMap: Record = { colorClass: "technique-command-and-control", }, - "Exfiltration": { + Exfiltration: { icon: Upload, colorClass: "technique-exfiltration", }, - "Impact": { + Impact: { icon: AlertTriangle, colorClass: "technique-impact", }, }; -function getTechniqueCardVisual( - tactic: string, -): TechniqueVisual { +function getTechniqueCardVisual(tactic: string): TechniqueVisual { return ( tacticVisualMap[tactic] ?? { icon: FileText, @@ -93,4 +91,4 @@ function getTechniqueCardVisual( ); } -export default getTechniqueCardVisual; \ No newline at end of file +export default getTechniqueCardVisual; diff --git a/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx b/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx index 685dc03..8f5888e 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueGrid/TechniqueGrid.tsx @@ -15,9 +15,7 @@ type TechniqueGridProps = { pageSize: number; techniquesCount: number; - onPageChange: ( - page: number, - ) => void; + onPageChange: (page: number) => void; }; function TechniqueGrid({ @@ -27,54 +25,45 @@ function TechniqueGrid({ techniquesCount, onPageChange, }: TechniqueGridProps) { - const totalPossiblePages = - Math.ceil( - techniquesCount / pageSize, - ); + const totalPossiblePages = Math.ceil(techniquesCount / pageSize); return ( - <> -
- {techniques.map((technique) => ( - - ))} -
+ <> +
+ {techniques.map((technique) => ( + + ))} +
-
- +
+ - - {techniquesCount === 0 - ? 0 - : (page - 1) * pageSize + 1} - – - {Math.min( - page * pageSize, - techniquesCount, - )} - + + {techniquesCount === 0 ? 0 : (page - 1) * pageSize + 1}– + {Math.min(page * pageSize, techniquesCount)} + - -
- -); + +
+ + ); } -export default TechniqueGrid; \ No newline at end of file +export default TechniqueGrid; diff --git a/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.css b/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.css index e22ce89..d5bb60b 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.css +++ b/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.css @@ -167,4 +167,4 @@ .technique-control-button:hover { background: var(--surface-hover); -} \ No newline at end of file +} diff --git a/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx b/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx index 8fa154c..8976e56 100644 --- a/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx +++ b/frontend/src/components/techniques/allTechniques/TechniqueToolbar/TechniqueToolbar.tsx @@ -1,8 +1,4 @@ -import { - Search, - ArrowUpDown, - ChevronDown, -} from "lucide-react"; +import { Search, ArrowUpDown, ChevronDown } from "lucide-react"; import "./TechniqueToolbar.css"; import PageHeader from "../../../common/PageHeader/PageHeader"; @@ -25,46 +21,30 @@ function TechniqueToolbar() { />
-
- - Filters - + Filters - -
- - Sort by - + Sort by -
-
-

{description}

-
+
+

{description}

+
); } diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.css b/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.css index a36c055..6ff5a23 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.css +++ b/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.css @@ -64,7 +64,7 @@ border-radius: 999px; - background: rgba(0, 0, 0, 0.946);; + background: rgba(0, 0, 0, 0.946); color: #6d3bf7; } @@ -75,4 +75,4 @@ background: rgba(0, 0, 0, 0.85); color: #c7ddff; -} \ No newline at end of file +} diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx b/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx index 2a6ea93..1551da8 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx +++ b/frontend/src/components/techniques/specificTechnique/TechniqueDetailedHeader/TechniqueDetailedHeader.tsx @@ -1,6 +1,5 @@ import { Star } from "lucide-react"; - import "./TechniqueDetailedHeader.css"; import BackButton from "../../../common/Navigation/BackButton"; @@ -20,10 +19,8 @@ function TechniqueDetailedHeader({ return (
- - - TECHNIQUE - + + TECHNIQUE
@@ -41,15 +38,10 @@ function TechniqueDetailedHeader({
- - {tactic} - + {tactic} {platforms.map((platform) => ( - + {platform} ))} @@ -58,4 +50,4 @@ function TechniqueDetailedHeader({ ); } -export default TechniqueDetailedHeader; \ No newline at end of file +export default TechniqueDetailedHeader; diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.css b/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.css index ad25410..b589f36 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.css +++ b/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.css @@ -25,4 +25,4 @@ .technique-overview-value { font-weight: 600; -} \ No newline at end of file +} diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx b/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx index fc290f2..5b96369 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx +++ b/frontend/src/components/techniques/specificTechnique/TechniqueOverview/TechniqueOverview.tsx @@ -25,21 +25,16 @@ function TechniqueOverview({

Related Tactic: {tactic}

-

- Platforms: {platform.join(", ")} -

+

Platforms: {platform.join(", ")}

Parent Technique: None

- Sub-techniques: { - subTechniqueCount > 0 - ? `Yes (${subTechniqueCount})` - : "No" - } + Sub-techniques:{" "} + {subTechniqueCount > 0 ? `Yes (${subTechniqueCount})` : "No"}

); } -export default TechniqueOverview; \ No newline at end of file +export default TechniqueOverview; diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.css b/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.css index 86339fd..7e6f40d 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.css +++ b/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.css @@ -20,9 +20,9 @@ } .sub-technique-card { - display: flex; - justify-content: space-between; - align-items: center; + display: flex; + justify-content: space-between; + align-items: center; } .sub-technique-left { @@ -33,4 +33,4 @@ .sub-technique-card + .sub-technique-card { margin-top: 10px; -} \ No newline at end of file +} diff --git a/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.tsx b/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.tsx index 04640ae..c119d6b 100644 --- a/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.tsx +++ b/frontend/src/components/techniques/specificTechnique/TechniqueSubTechnique/SubTechniques.tsx @@ -10,9 +10,7 @@ type SubTechniquesProps = { subTechniques: SubTechnique[]; }; -function SubTechniques({ - subTechniques, -}: SubTechniquesProps) { +function SubTechniques({ subTechniques }: SubTechniquesProps) { return (
@@ -22,10 +20,7 @@ function SubTechniques({
{subTechniques.map((technique) => ( -
+
@@ -41,4 +36,4 @@ function SubTechniques({ ); } -export default SubTechniques; \ No newline at end of file +export default SubTechniques; diff --git a/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.css b/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.css index 2f0296c..255771d 100644 --- a/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.css +++ b/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.css @@ -26,7 +26,6 @@ transform: translateY(-2px); } - .threat-actor-card-header { display: flex; align-items: flex-start; @@ -156,43 +155,43 @@ /* ACTIVE – grønn */ .threat-actor-status.active { - background-color: rgba(0, 200, 120, 0.15); - color: #00d89c; + background-color: rgba(0, 200, 120, 0.15); + color: #00d89c; } .threat-actor-status.active .status-dot { - background-color: #00d89c; + background-color: #00d89c; } /* INACTIVE – rød */ .threat-actor-status.inactive { - background-color: rgba(239, 68, 68, 0.15); - color: #f87171; + background-color: rgba(239, 68, 68, 0.15); + color: #f87171; } .threat-actor-status.inactive .status-dot { - background-color: #f87171; + background-color: #f87171; } /* UNKNOWN – grå */ .threat-actor-status.unknown { - background-color: rgba(148, 163, 184, 0.15); - color: #94a3b8; + background-color: rgba(148, 163, 184, 0.15); + color: #94a3b8; } .threat-actor-status.unknown .status-dot { - background-color: #94a3b8; + background-color: #94a3b8; } .threat-actor-details { - margin-top: auto; + margin-top: auto; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; } .open-profile { - margin-top: 0; -} \ No newline at end of file + margin-top: 0; +} diff --git a/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.tsx b/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.tsx index ef08cdc..5e8f08f 100644 --- a/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.tsx +++ b/frontend/src/components/threatactor/catalog/ThreatActorCard/ThreatActorCard.tsx @@ -4,78 +4,78 @@ import ReactCountryFlag from "react-country-flag"; import "./ThreatActorCard.css"; type ThreatActorCardProps = { - name: string; - mitre_id: string; - alias: string; - country: string; - country_code?: string; - motivation: string; - status: string; -} + name: string; + mitre_id: string; + alias: string; + country: string; + country_code?: string; + motivation: string; + status: string; +}; -function ThreatActorCard ({ - name, - mitre_id, - alias, - country, - country_code, - motivation, - status -}: ThreatActorCardProps){ - return ( -
-
- +function ThreatActorCard({ + name, + mitre_id, + alias, + country, + country_code, + motivation, + status, +}: ThreatActorCardProps) { + return ( +
+
+ -
-

{name}

-

Also known as {alias}

-
+
+

{name}

+

Also known as {alias}

+
- {mitre_id} -
+ {mitre_id} +
-
-
- - {status} -
+
+
+ + {status} +
-
-
- {country_code ? ( - - ) : ( - - )} +
+
+ {country_code ? ( + + ) : ( + + )} - {country} -
+ {country} +
-
- - {motivation} -
-
-
+
+ + {motivation} +
+
+
- - Open profile → - -
- ); + + Open profile → + +
+ ); } -export default ThreatActorCard; \ No newline at end of file +export default ThreatActorCard; diff --git a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.css b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.css index 42c33e9..19ca2b3 100644 --- a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.css +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.css @@ -1,4 +1,3 @@ - /* Rad med aktive søk, filtre og sortering */ .active-threat-actor-filters { display: flex; @@ -50,17 +49,12 @@ background: transparent; color: var(--text-muted); border: none; - color: #b8cccc; -font-size: 13px; -font-weight: 500; + color: #b8cccc; + font-size: 13px; + font-weight: 500; font-size: 12px; cursor: pointer; - - - - - } .active-filter-clear:hover { diff --git a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.tsx b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.tsx index b99d524..4669c2b 100644 --- a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.tsx +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorActiveFilters.tsx @@ -1,8 +1,6 @@ - import { X } from "lucide-react"; -import type { ThreatActorFilterValues } from "../ThreatActorFilter/ThreatActorFilter"; import "./ThreatActorActiveFilters.css"; - +import type { ThreatActorFilterValues } from "../../../../types/ThreatActorTypes"; // Alle filterfelter og navnene som skal vises const filterFields = [ @@ -31,18 +29,27 @@ type ActiveFiltersProps = { onClearAll: () => void; }; -export default function ThreatActorActiveFilters({search, filters, onRemoveSearch, onRemoveFilter, onClearAll}: ActiveFiltersProps) { - +export default function ThreatActorActiveFilters({ + search, + filters, + onRemoveSearch, + onRemoveFilter, + onClearAll, +}: ActiveFiltersProps) { // Finn filtrene som faktisk er aktive - const activeFilters = filterFields.filter(({ key }) => key === "sortBy" ? filters.sortBy !== "name-asc" : filters[key] !== ""); + const activeFilters = filterFields.filter(({ key }) => + key === "sortBy" ? filters.sortBy !== "name-asc" : filters[key] !== "", + ); // Ikke vis raden hvis ingen kriterier er aktive if (!search && activeFilters.length === 0) return null; return ( -
+
- {/* Search-chip */} {search && ( ))}
-
diff --git a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.css b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.css index 1dc2d99..fa8d61a 100644 --- a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.css +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.css @@ -1,4 +1,3 @@ - .threat-actor-filters { display: flex; flex-direction: column; @@ -112,4 +111,4 @@ .reset-filters-button:focus-visible { outline: 2px solid var(--text-heading); outline-offset: 3px; -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.tsx b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.tsx index f4292b2..0725724 100644 --- a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.tsx +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilter.tsx @@ -12,7 +12,11 @@ import { useState } from "react"; import FilterDropdown from "./ThreatActorFilterDropdown"; import type { FilterOption } from "./ThreatActorFilterDropdown"; import "./ThreatActorFilter.css"; -import { DEFAULT_FILTERS, type ThreatActorFilterOptionsData, type ThreatActorFilterValues } from "../../../../types/ThreatActorTypes"; +import { + DEFAULT_FILTERS, + type ThreatActorFilterOptionsData, + type ThreatActorFilterValues, +} from "../../../../types/ThreatActorTypes"; const sortOptions: FilterOption[] = [ { value: "name-asc", label: "Name (A - Z)" }, @@ -21,46 +25,50 @@ const sortOptions: FilterOption[] = [ { value: "mitre-desc", label: "MITRE ID (Descending)" }, ]; - type ThreatActorFiltersProps = { - filterOptions?: ThreatActorFilterOptionsData["threatActorFilterOptions"]; - value: ThreatActorFilterValues; - onChange: (filters: ThreatActorFilterValues) => void; - onApply: (filters: ThreatActorFilterValues) => void; + filterOptions?: ThreatActorFilterOptionsData["threatActorFilterOptions"]; + value: ThreatActorFilterValues; + onChange: (filters: ThreatActorFilterValues) => void; + onApply: (filters: ThreatActorFilterValues) => void; }; - - function ThreatActorFilters({ - value, - onChange, - onApply, - filterOptions + value, + onChange, + onApply, + filterOptions, }: ThreatActorFiltersProps) { + const [openFilter, setOpenFilter] = useState(null); - const [openFilter, setOpenFilter] = useState(null); + const { country, region, motivation, sector, status, dangerLevel, sortBy } = + value; - const {country,region, motivation, sector, status, dangerLevel, sortBy} = value; + // Oppdaterer ett bestemt filter + const updateFilter = ( + key: keyof ThreatActorFilterValues, + newValue: string, + ) => { + onChange({ ...value, [key]: newValue }); + }; - // Oppdaterer ett bestemt filter - const updateFilter = (key: keyof ThreatActorFilterValues, newValue: string) => {onChange({...value, [key]: newValue})}; + const toggleFilter = (filter: string) => { + setOpenFilter((previous) => (previous === filter ? null : filter)); + }; - const toggleFilter = (filter: string) => {setOpenFilter((previous) => previous === filter ? null : filter)}; + const handleApplyFilters = () => { + setOpenFilter(null); + onApply(value); + }; - const handleApplyFilters = () => { - setOpenFilter(null); - onApply(value); - }; + const handleResetFilters = () => { + setOpenFilter(null); - const handleResetFilters = () => { - setOpenFilter(null); + const resetFilters = { ...DEFAULT_FILTERS }; - const resetFilters = { ...DEFAULT_FILTERS }; + onChange(resetFilters); + onApply(resetFilters); + }; - onChange(resetFilters); - onApply(resetFilters); - }; - return (

Filters

@@ -144,28 +152,28 @@ function ThreatActorFilters({ showAllOption={false} /> -
- - - -
+
+ + + +
); } -export default ThreatActorFilters; \ No newline at end of file +export default ThreatActorFilters; diff --git a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.css b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.css index a11c218..21f4a9e 100644 --- a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.css +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.css @@ -1,4 +1,3 @@ - .filter-dropdown { position: relative; min-width: 0; diff --git a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.tsx b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.tsx index 884840f..08b7ec3 100644 --- a/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.tsx +++ b/frontend/src/components/threatactor/catalog/ThreatActorFilter/ThreatActorFilterDropdown.tsx @@ -19,19 +19,30 @@ type FilterDropdownProps = { showAllOption?: boolean; }; -export default function FilterDropdown({label, placeholder,icon,options,value,onChange,isOpen,onToggle,showAllOption = true}: FilterDropdownProps) { +export default function FilterDropdown({ + label, + placeholder, + icon, + options, + value, + onChange, + isOpen, + onToggle, + showAllOption = true, +}: FilterDropdownProps) { // Sorterer filteralternativer etter antall treff. // Alternativer uten count (f.eks. Sort by) beholder sin opprinnelige rekkefølge. - const sortedOptions = [...options].sort((a, b) => { + const sortedOptions = [...options].sort((a, b) => { if (a.count === undefined || b.count === undefined) { - return 0; + return 0; } return b.count - a.count || a.label.localeCompare(b.label); - }); + }); // Viser valgt alternativ i dropdown-knappen. - const selectedLabel =options.find((option) => option.value === value)?.label ?? placeholder; + const selectedLabel = + options.find((option) => option.value === value)?.label ?? placeholder; return (
); -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.css b/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.css index 86d55b8..9db71e3 100644 --- a/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.css +++ b/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.css @@ -6,8 +6,7 @@ display: grid; - grid-template-columns: - repeat(auto-fit, minmax(300px, 340px)); + grid-template-columns: repeat(auto-fit, minmax(300px, 340px)); justify-content: center; @@ -62,4 +61,4 @@ .threat-actor-pagination button:disabled { opacity: 0.4; cursor: not-allowed; -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.tsx b/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.tsx index c30bf87..1bd9d7d 100644 --- a/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.tsx +++ b/frontend/src/components/threatactor/catalog/ThreatActorGrid/ThreatActorGrid.tsx @@ -3,7 +3,6 @@ import type { ThreatActorGridItem } from "../../../../types/ThreatActorTypes"; import "./ThreatActorGrid.css"; import { getCountryCode } from "../../../../utils/getCountryCode"; - type ThreatActorGridProps = { actors: ThreatActorGridItem[]; page: number; @@ -11,8 +10,13 @@ type ThreatActorGridProps = { actorsCount: number; onPageChange: (page: number) => void; }; -function ThreatActorGrid({actors, page, pageSize, actorsCount, onPageChange }: ThreatActorGridProps) { - +function ThreatActorGrid({ + actors, + page, + pageSize, + actorsCount, + onPageChange, +}: ThreatActorGridProps) { //math.ceil(190/9) => math.ceil(21.11) => 22: mulige pages (siste page har bare 1 ThreatActorCard) const totalPossiblePages = Math.ceil(actorsCount / pageSize); @@ -20,7 +24,7 @@ function ThreatActorGrid({actors, page, pageSize, actorsCount, onPageChange }: T <>
{actors.map((actor) => ( -
- - - {actorsCount === 0 ? 0 : (page - 1) * pageSize + 1}–{Math.min(page * pageSize, actorsCount)} + + {actorsCount === 0 ? 0 : (page - 1) * pageSize + 1}– + {Math.min(page * pageSize, actorsCount)} + -
@@ -49,4 +63,4 @@ function ThreatActorGrid({actors, page, pageSize, actorsCount, onPageChange }: T ); } -export default ThreatActorGrid; \ No newline at end of file +export default ThreatActorGrid; diff --git a/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.css b/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.css index 97941b5..bb47ab0 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.css @@ -1,4 +1,3 @@ - /* ===== Associated panel ===== */ .associated-panel { @@ -16,7 +15,6 @@ border-radius: 8px; } - /* ===== Panel header ===== */ .associated-panel-header { @@ -52,7 +50,6 @@ font-weight: 700; } - /* ===== Associated list ===== */ .associated-list { @@ -61,7 +58,6 @@ gap: 6px; } - /* ===== Associated row ===== */ .associated-row { @@ -80,7 +76,6 @@ border-radius: 7px; } - /* ===== Shared icon ===== */ .associated-row-icon { @@ -101,7 +96,6 @@ height: 18px; } - /* ===== MITRE ID ===== */ .associated-item-id { @@ -117,7 +111,6 @@ white-space: nowrap; } - /* ===== Item name ===== */ .associated-item-name { @@ -132,7 +125,6 @@ overflow-wrap: anywhere; } - /* ===== Row arrow ===== */ .associated-row-arrow { @@ -145,7 +137,6 @@ color: var(--text); } - /* ===== Techniques palette ===== */ .associated-row-technique .associated-row-icon { @@ -158,7 +149,6 @@ color: var(--accent); } - /* ===== Software palette ===== */ .associated-row-software .associated-row-icon { @@ -171,7 +161,6 @@ color: #ff6b6b; } - /* ===== Pagination ===== */ .associated-pagination { @@ -224,7 +213,6 @@ outline-offset: 2px; } - /* ===== Mobile ===== */ @media (max-width: 520px) { @@ -277,16 +265,16 @@ } .associated-row, .associated-row:visited { - text-decoration: none; - color: inherit; + text-decoration: none; + color: inherit; } .associated-row:hover { - border-color: var(--accent); - cursor: pointer; + border-color: var(--accent); + cursor: pointer; } .associated-row:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; -} \ No newline at end of file + outline: 2px solid var(--accent); + outline-offset: 2px; +} diff --git a/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx b/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx index 947d420..a943abd 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorAssociated/ThreatActorAssociatedList.tsx @@ -1,9 +1,4 @@ -import { - ChevronLeft, - ChevronRight, - FileText, - Settings, -} from "lucide-react"; +import { ChevronLeft, ChevronRight, FileText, Settings } from "lucide-react"; import type { AssociatedItem } from "../../../../types/ThreatActorTypes"; import "./ThreatActorAssociatedList.css"; import FeedbackCard from "../../../common/FeedBack/FeedbackCard"; @@ -18,7 +13,14 @@ type ThreatActorAssociatedListProps = { onPageChange: (page: number) => void; }; -function ThreatActorAssociatedList({type,items,totalCount,page,pageSize,onPageChange}: ThreatActorAssociatedListProps) { +function ThreatActorAssociatedList({ + type, + items, + totalCount, + page, + pageSize, + onPageChange, +}: ThreatActorAssociatedListProps) { const isTechnique = type === "techniques"; const totalPages = Math.ceil(totalCount / pageSize); @@ -32,47 +34,48 @@ function ThreatActorAssociatedList({type,items,totalCount,page,pageSize,onPageCh
- {/* Vis empty state dersom ingen tilknyttede entiteter finnes */} - {totalCount === 0 && ( - - )} + {/* Vis empty state dersom ingen tilknyttede entiteter finnes */} + {totalCount === 0 && ( + + )} {items.map((item) => ( - - - {isTechnique ? : } - + + + {isTechnique ? : } + - {item.mitreId} - {item.name} + {item.mitreId} + {item.name} - - + + ))} {totalPages > 1 && (
- @@ -80,7 +83,12 @@ function ThreatActorAssociatedList({type,items,totalCount,page,pageSize,onPageCh Page {page + 1} of {totalPages} -
@@ -89,4 +97,4 @@ function ThreatActorAssociatedList({type,items,totalCount,page,pageSize,onPageCh ); } -export default ThreatActorAssociatedList; \ No newline at end of file +export default ThreatActorAssociatedList; diff --git a/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css index 7663612..adeb9cf 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.css @@ -21,4 +21,4 @@ font-size: 14px; line-height: 1.6; -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx index 34486d9..d4264b2 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedDescription/ThreatActorDetailedDescription.tsx @@ -15,4 +15,4 @@ function ThreatActorDetailedDescription({ ); } -export default ThreatActorDetailedDescription; \ No newline at end of file +export default ThreatActorDetailedDescription; diff --git a/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css index 5831469..0180542 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.css @@ -6,7 +6,6 @@ gap: 14px; } - /* ===== Back link ===== */ .back-button { @@ -23,7 +22,6 @@ text-decoration: underline; } - /* ===== Actor title ===== */ .detailed-title-row { @@ -53,7 +51,6 @@ font-weight: 700; } - /* ===== Favorite button ===== */ .favorite-button { @@ -82,7 +79,6 @@ color: var(--accent); } - /* ===== Aliases ===== */ .aliases { @@ -111,4 +107,4 @@ color: var(--text); font-size: 13px; -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx index 9149a69..918fb2c 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedHeader/ThreatActorDetailedHeader.tsx @@ -10,46 +10,45 @@ type ThreatActorDetailsHeaderProps = { }; function ThreatActorDetailedHeader({ - name, - mitreId, - aliases, - targetSectors - }: ThreatActorDetailsHeaderProps){ - return ( -
- - -
-

{name}

- - {mitreId} - -
- -
- Also known as: - - {aliases.map((alias) => ( - {alias} - ))} -
- -
- Target Sectors: - - {targetSectors.map((sector) => ( - {sector} - ))} -
-
- - ); + name, + mitreId, + aliases, + targetSectors, +}: ThreatActorDetailsHeaderProps) { + return ( +
+ + +
+

{name}

+ + {mitreId} + +
+ +
+ Also known as: + + {aliases.map((alias) => ( + {alias} + ))} +
+ +
+ Target Sectors: + + {targetSectors.map((sector) => ( + {sector} + ))} +
+
+ ); } -export default ThreatActorDetailedHeader; \ No newline at end of file +export default ThreatActorDetailedHeader; diff --git a/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css index 91668c6..af17230 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.css @@ -15,7 +15,6 @@ border-radius: 8px; } - /* ===== Icon ===== */ .detailed-info-card-icon { @@ -33,7 +32,6 @@ height: 28px; } - /* ===== Text content ===== */ .detailed-info-card-content { @@ -57,4 +55,4 @@ font-size: 14px; font-weight: 500; line-height: 1.3; -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx index 53cf7eb..b5d0eff 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCard.tsx @@ -3,11 +3,11 @@ import "./ThreatActorDetailedInfoCard.css"; /** * Tar inn en ReactNode icons som - * + * * Tar inn et navn på boksen - * - * Tar inn verdien fra DB som vises - * + * + * Tar inn verdien fra DB som vises + * * Tar inn en type som brukes for spesifikk styling (optional) */ type ThreatActorDetailedInfoCardProps = { @@ -17,7 +17,6 @@ type ThreatActorDetailedInfoCardProps = { variant?: string; }; - function ThreatActorDetailedInfoCard({ icon, title, @@ -26,21 +25,15 @@ function ThreatActorDetailedInfoCard({ }: ThreatActorDetailedInfoCardProps) { return (
-
- {icon} -
+
{icon}
- - {title} - + {title} -
- {value} -
+
{value}
); } -export default ThreatActorDetailedInfoCard; \ No newline at end of file +export default ThreatActorDetailedInfoCard; diff --git a/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css index b9f1569..47edac7 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.css @@ -6,7 +6,6 @@ gap: 12px; } - /* ===== Country ===== */ .detailed-info-country { @@ -20,8 +19,6 @@ height: 16px !important; } - - /* ===== Status ===== */ .status-info-card.status-active { @@ -41,7 +38,6 @@ color: var(--status-color); } - /* ===== Danger Level ===== */ /* Farger basert på risikonivå */ @@ -82,7 +78,6 @@ font-weight: 700; } - /* ===== Smaller desktop ===== */ @media (max-width: 1200px) { @@ -91,7 +86,6 @@ } } - /* ===== Tablet ===== */ @media (max-width: 900px) { @@ -104,7 +98,6 @@ } } - /* ===== Mobile ===== */ @media (max-width: 520px) { @@ -116,4 +109,4 @@ .target-sectors-card { grid-column: auto; } -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx index 814f832..2950809 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedInfoCards/ThreatActorDetailedInfoCards.tsx @@ -1,9 +1,8 @@ -import {AlertTriangle,Circle,Flag,MapPin} from "lucide-react"; +import { AlertTriangle, Circle, Flag, MapPin } from "lucide-react"; import ReactCountryFlag from "react-country-flag"; import ThreatActorDetailedInfoCard from "./ThreatActorDetailedInfoCard"; import "./ThreatActorDetailedInfoCards.css"; - type ThreatActorDetailedInfoCardsProps = { country: string; countryCode?: string; @@ -21,9 +20,15 @@ type ThreatActorDetailedInfoCardsProps = { *├── data: sectors *├── data: status *└── data: danger level - * + * */ -function ThreatActorDetailedInfoCards({country,countryCode,motivation,status,dangerLevel}: ThreatActorDetailedInfoCardsProps) { +function ThreatActorDetailedInfoCards({ + country, + countryCode, + motivation, + status, + dangerLevel, +}: ThreatActorDetailedInfoCardsProps) { return (
{countryCode ? ( - + ) : ( )} @@ -68,4 +69,4 @@ function ThreatActorDetailedInfoCards({country,countryCode,motivation,status,dan ); } -export default ThreatActorDetailedInfoCards; \ No newline at end of file +export default ThreatActorDetailedInfoCards; diff --git a/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.css b/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.css index a87453e..fc91735 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.css +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.css @@ -16,7 +16,6 @@ font-weight: 700; } - /* ===== Overview data ===== */ .overview-list { @@ -47,7 +46,6 @@ font-size: 14px; } - /* ===== Country ===== */ .overview-country { @@ -61,8 +59,6 @@ height: 16px !important; } - - /* ===== Status ===== */ .overview-status { @@ -81,11 +77,8 @@ color: #9ca3af; } - - /* ===== Danger Level ===== */ - .overview-row dd.overview-danger { width: fit-content; padding: 3px 10px; @@ -120,7 +113,6 @@ --danger-background: rgba(239, 68, 68, 0.18); } - /* ===== MITRE link ===== */ .mitre-link { @@ -143,7 +135,6 @@ text-decoration: underline; } - /* ===== Laptop: Overview is below the main content ===== */ @media (max-width: 1500px) and (min-width: 1101px) { @@ -153,7 +144,6 @@ } } - /* ===== Mobile ===== */ @media (max-width: 520px) { @@ -161,4 +151,4 @@ grid-template-columns: 1fr; gap: 4px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx b/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx index d94151d..902565f 100644 --- a/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx +++ b/frontend/src/components/threatactor/detailed/ThreatActorDetailedOverview/ThreatActorOverview.tsx @@ -25,7 +25,7 @@ function ThreatActorOverview({ dangerLevel, firstSeen, lastSeen, - xMitreDeprecated // Ny + xMitreDeprecated, // Ny }: ThreatActorOverviewProps) { return (
-
-

- Showing {firstVisibleEntry}-{lastVisibleEntry} of {visibleEntries.length} software entries -

+
+

+ Showing{" "} + + {firstVisibleEntry}-{lastVisibleEntry} + {" "} + of {visibleEntries.length} software entries +

- {loading ? ( -
-
- ) : loadError ? ( -
-
- ) : visibleEntries.length > 0 ? ( -
- {pageEntries.map((entry, index) => { - const Icon = - entry.type === "TOOL" - ? index % 2 === 0 - ? TerminalSquare - : Radio - : Bug; + {loading ? ( +
+
+ ) : loadError ? ( +
+
+ ) : visibleEntries.length > 0 ? ( +
+ {pageEntries.map((entry, index) => { + const Icon = + entry.type === "TOOL" + ? index % 2 === 0 + ? TerminalSquare + : Radio + : Bug; - return ( -
-
-
-
- {entry.mitreId} - - {entry.type === "TOOL" ? "Tool" : "Malware"} - -
-

{entry.name}

-

{("summary" in entry && entry.summary) || entry.description}

-

- {entry.platforms.join(" · ")} -

- - Open profile
- ); - })} -
- ) : ( -
-
- )} + return ( +
+
+
+
+ {entry.mitreId} + + {entry.type === "TOOL" ? "Tool" : "Malware"} + +
+

{entry.name}

+

+ {("summary" in entry && entry.summary) || entry.description} +

+

+ {entry.platforms.join(" · ")} +

+ + Open profile
+ ); + })} +
+ ) : ( +
+
+ )} - {!loading && !loadError && pageCount > 1 && ( - - )} -
- - ); + {!loading && !loadError && pageCount > 1 && ( + + )} +
+ + ); } -export default Software; \ No newline at end of file +export default Software; diff --git a/frontend/src/pages/Software/SoftwareDetail.css b/frontend/src/pages/Software/SoftwareDetail.css index a561c64..67d952f 100644 --- a/frontend/src/pages/Software/SoftwareDetail.css +++ b/frontend/src/pages/Software/SoftwareDetail.css @@ -133,7 +133,11 @@ .software-overview { border: 1px solid var(--border); border-radius: 7px; - background: linear-gradient(145deg, rgba(0, 32, 34, 0.94), rgba(0, 19, 21, 0.94)); + background: linear-gradient( + 145deg, + rgba(0, 32, 34, 0.94), + rgba(0, 19, 21, 0.94) + ); } .software-detail-description { @@ -387,4 +391,4 @@ a.software-related-name:hover { grid-template-columns: 61px minmax(0, 1fr) 14px; gap: 7px; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/Software/SoftwareDetail.tsx b/frontend/src/pages/Software/SoftwareDetail.tsx index ff2486f..3e7a515 100644 --- a/frontend/src/pages/Software/SoftwareDetail.tsx +++ b/frontend/src/pages/Software/SoftwareDetail.tsx @@ -1,5 +1,13 @@ import { useEffect, useState } from "react"; -import { ArrowLeft, ArrowRight, Bug, Database, Network, Radio, TerminalSquare } from "lucide-react"; +import { + ArrowLeft, + ArrowRight, + Bug, + Database, + Network, + Radio, + TerminalSquare, +} from "lucide-react"; import { Link, useParams } from "react-router-dom"; import { getSoftwareByMitreId } from "../../data/softwareData"; import type { SoftwareEntry } from "../../data/softwareData"; @@ -40,7 +48,10 @@ function SoftwareDetail() { mitreId, status: "error", software: null, - error: requestError instanceof Error ? requestError.message : "Unable to load software details.", + error: + requestError instanceof Error + ? requestError.message + : "Unable to load software details.", }); } }); @@ -50,8 +61,10 @@ function SoftwareDetail() { }; }, [mitreId]); - const loading = detailState.mitreId !== mitreId || detailState.status === "loading"; - const software = detailState.mitreId === mitreId ? detailState.software : null; + const loading = + detailState.mitreId !== mitreId || detailState.status === "loading"; + const software = + detailState.mitreId === mitreId ? detailState.software : null; const error = detailState.mitreId === mitreId ? detailState.error : ""; if (loading) { @@ -72,9 +85,14 @@ function SoftwareDetail() {
-
+

Description

-

{cleanDescription(software.description) || "No description is available."}

+

+ {cleanDescription(software.description) || + "No description is available."} +

-
+
- {software.associatedTechniques.length} + + {software.associatedTechniques.length} +
{software.associatedTechniques.length ? (
    {software.associatedTechniques .slice(0, showAllTechniques ? undefined : 5) .map((technique) => ( -
  • - {technique.mitreId} - {technique.name} -
  • - ))} +
  • + + {technique.mitreId} + + + {technique.name} + +
  • + ))}
) : ( -

No associated techniques are recorded.

+

+ No associated techniques are recorded. +

)} {software.associatedTechniques.length > 5 && ( )}
-
+
- {software.usedByThreatActors.length} + + {software.usedByThreatActors.length} +
{software.usedByThreatActors.length ? (
    {software.usedByThreatActors .slice(0, showAllActors ? undefined : 5) .map((actor) => ( -
  • - {actor.mitreId} - - {actor.name} - -
  • - ))} +
  • + + {actor.mitreId} + + + {actor.name} + +
  • + ))}
) : ( -

No threat actor relationships are recorded.

+

+ No threat actor relationships are recorded. +

)} {software.usedByThreatActors.length > 5 && ( )}
-