From e21783e050b4cb4577cb10fd2ac5dc83b62b1c5a Mon Sep 17 00:00:00 2001 From: omdxp Date: Mon, 23 Dec 2024 19:22:48 +0100 Subject: [PATCH 01/19] feat: add search input to top bar with localization support --- web/src/components/locale/dictionary.ts | 1 + web/src/components/top-bar.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/web/src/components/locale/dictionary.ts b/web/src/components/locale/dictionary.ts index a8e392729..82c7d33f8 100644 --- a/web/src/components/locale/dictionary.ts +++ b/web/src/components/locale/dictionary.ts @@ -16,6 +16,7 @@ export const dictionary = { "navbar-section-projects": { en: "Projects", ar: "مشاريع" }, "navbar-section-articles": { en: "Articles", ar: "مقالات" }, "navbar-section-faq": { en: "FAQ", ar: "أسئلة / أجوبة" }, + "navbar-section-search": { en: "Search...", ar: "بحث..." }, "footer-category-title-helpful-links": { en: "Helpful Links", diff --git a/web/src/components/top-bar.tsx b/web/src/components/top-bar.tsx index e27dad6b5..43988b523 100644 --- a/web/src/components/top-bar.tsx +++ b/web/src/components/top-bar.tsx @@ -5,7 +5,7 @@ import logoWide from "src/assets/svg/logo-wide.svg"; import logoWideExtended from "src/assets/svg/logo-wide-extended.svg"; import { Image } from "src/components/image"; import { Link } from "src/components/link"; -import { Locale } from "src/components/locale"; +import { Locale, useLocale } from "src/components/locale"; import { DictionaryKeys } from "src/components/locale/dictionary"; import { changeLanguage } from "src/redux/actions/settings"; import { useAppSelector } from "src/redux/store"; @@ -37,6 +37,8 @@ export function TopBar({ version, links }: TopBarProps): JSX.Element { return { selectedLanguage, languageOptions }; }, [selectedLanguageCode]); + const { localize } = useLocale(); + return (
@@ -51,6 +53,11 @@ export function TopBar({ version, links }: TopBarProps): JSX.Element { {version}
+
{selectedLanguage.label} From 9c939c8ba220da1cf24950861e4e7f3a6508d2c8 Mon Sep 17 00:00:00 2001 From: omdxp Date: Mon, 23 Dec 2024 21:23:52 +0100 Subject: [PATCH 02/19] feat: implement search modal and enhance top bar with search functionality --- web/src/_entry/app.tsx | 18 +++- web/src/components/search.tsx | 72 ++++++++++++++++ web/src/components/top-bar.tsx | 16 +++- .../contribute/contribute-card/index.tsx | 79 +++++++++++++++++ web/src/pages/contribute/index.tsx | 59 +------------ web/src/pages/projects/index.tsx | 68 +-------------- web/src/pages/projects/project-card/index.tsx | 84 +++++++++++++++++++ web/src/pages/team/contributor-card/index.tsx | 84 +++++++++++++++++++ web/src/pages/team/index.tsx | 69 +-------------- 9 files changed, 355 insertions(+), 194 deletions(-) create mode 100644 web/src/components/search.tsx create mode 100644 web/src/pages/contribute/contribute-card/index.tsx create mode 100644 web/src/pages/projects/project-card/index.tsx create mode 100644 web/src/pages/team/contributor-card/index.tsx diff --git a/web/src/_entry/app.tsx b/web/src/_entry/app.tsx index faf09521e..3b55c9398 100644 --- a/web/src/_entry/app.tsx +++ b/web/src/_entry/app.tsx @@ -8,7 +8,8 @@ import { Languages } from "src/components/locale/languages"; import { TopBar, TopBarProps } from "src/components/top-bar"; import { StoreProvider } from "src/redux/store"; import { getInitialLanguageCode } from "src/utils/website-language"; -import React from "react"; +import React, { useCallback, useEffect } from "react"; +import { Search } from "src/components/search"; let routes: Array< RouteProps & { @@ -116,9 +117,24 @@ const topBarLinks: TopBarProps["links"] = [ ]; const App = () => { + const onKeyDown = useCallback((event: KeyboardEvent) => { + if (event.key === "/") { + event.preventDefault(); + (document.getElementById("search-modal") as HTMLDialogElement).showModal(); + } + }, []); + + useEffect(() => { + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("keydown", onKeyDown); + }; + }, [onKeyDown]); + return ( <>
+ {routes.map((route) => { diff --git a/web/src/components/search.tsx b/web/src/components/search.tsx new file mode 100644 index 000000000..bab7826b4 --- /dev/null +++ b/web/src/components/search.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { useLocale } from "./locale"; +import { useAppSelector } from "src/redux/store"; +import ProjectCard from "src/pages/projects/project-card"; +import ContributorCard from "src/pages/team/contributor-card"; +import ContributionCard from "src/pages/contribute/contribute-card"; + +export function Search(): JSX.Element { + const { localize } = useLocale(); + // @TODO-OB: use the search results from the API call (these are just placeholders) + const { projectsList } = useAppSelector((state) => state.projectsPage); + const { contributorsList } = useAppSelector((state) => state.contributorsPage); + const { contributionsList } = useAppSelector((state) => state.contributionsPage); + + return ( + +
+ +
+

Contributions

+
+ {contributionsList !== "ERROR" && + contributionsList + ?.slice(0, 5) + .map((contribution, contributionIndex) => ( + + ))} +
+
+
+

Contributors

+
+ {contributorsList !== "ERROR" && + contributorsList + ?.slice(0, 5) + .map((contributor, contributorIndex) => ( + + ))} +
+
+
+

Projects

+
+ {projectsList !== "ERROR" && + projectsList + ?.slice(0, 5) + .map((project, projectIndex) => ( + + ))} +
+
+
+
+ +
+
+ ); +} diff --git a/web/src/components/top-bar.tsx b/web/src/components/top-bar.tsx index 43988b523..c1d3d8f08 100644 --- a/web/src/components/top-bar.tsx +++ b/web/src/components/top-bar.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useCallback } from "react"; import { useMemo } from "react"; import { useLocation } from "react-router-dom"; import logoWide from "src/assets/svg/logo-wide.svg"; @@ -39,6 +39,10 @@ export function TopBar({ version, links }: TopBarProps): JSX.Element { const { localize } = useLocale(); + const showModal = useCallback(() => { + (document.getElementById("search-modal") as HTMLDialogElement).showModal(); + }, []); + return (
@@ -54,9 +58,13 @@ export function TopBar({ version, links }: TopBarProps): JSX.Element {
diff --git a/web/src/pages/contribute/contribute-card/index.tsx b/web/src/pages/contribute/contribute-card/index.tsx new file mode 100644 index 000000000..8a37d83a9 --- /dev/null +++ b/web/src/pages/contribute/contribute-card/index.tsx @@ -0,0 +1,79 @@ +import { ContributionEntity } from "@dzcode.io/models/dist/contribution"; +import { ContributorEntity } from "@dzcode.io/models/dist/contributor"; +import { ProjectEntity } from "@dzcode.io/models/dist/project"; +import { RepositoryEntity } from "@dzcode.io/models/dist/repository"; +import React from "react"; +import { Link } from "src/components/link"; +import { useLocale } from "src/components/locale"; +import { Markdown } from "src/components/markdown"; +import { getElapsedTime } from "src/utils/elapsed-time"; + +export default function ContributionCard({ + key, + contribution, +}: { + key: React.Key; + contribution: Pick< + ContributionEntity, + "id" | "title" | "type" | "url" | "updatedAt" | "activityCount" + > & { + repository: Pick & { + project: Pick; + }; + contributor: Pick; + }; +}) { + const { localize } = useLocale(); + + return ( +
+
+
+

+ +

+ + {contribution.repository.project.name} + + {contribution.repository.owner}/{contribution.repository.name} + +
+ +
+ {contribution.activityCount > 0 && ( +
+ + + + {contribution.activityCount} +
+ )} +
+ {getElapsedTime(contribution.updatedAt, localize("elapsed-time-suffixes"))} +
+ + {contribution.type === "ISSUE" + ? localize("contribute-read-issue") + : localize("contribute-review-changes")} + +
+
+
+
+ ); +} diff --git a/web/src/pages/contribute/index.tsx b/web/src/pages/contribute/index.tsx index ccd29a3cf..09d03fc8c 100644 --- a/web/src/pages/contribute/index.tsx +++ b/web/src/pages/contribute/index.tsx @@ -1,14 +1,12 @@ +import React from "react"; import { useEffect } from "react"; import { Helmet } from "react-helmet-async"; -import { Link } from "src/components/link"; import { Loading } from "src/components/loading"; import { Locale, useLocale } from "src/components/locale"; -import { Markdown } from "src/components/markdown"; import { TryAgain } from "src/components/try-again"; import { fetchContributionsListAction } from "src/redux/actions/contributions"; import { useAppDispatch, useAppSelector } from "src/redux/store"; -import { getElapsedTime } from "src/utils/elapsed-time"; -import React from "react"; +import ContributionCard from "./contribute-card"; // ts-prune-ignore-next export default function Page(): JSX.Element { @@ -45,58 +43,7 @@ export default function Page(): JSX.Element { ) : (
{contributionsList.map((contribution, contributionIndex) => ( -
-
-
-

- -

- - {contribution.repository.project.name} - - {contribution.repository.owner}/{contribution.repository.name} - -
- -
- {contribution.activityCount > 0 && ( -
- - - - {contribution.activityCount} -
- )} -
- {getElapsedTime(contribution.updatedAt, localize("elapsed-time-suffixes"))} -
- - {contribution.type === "ISSUE" - ? localize("contribute-read-issue") - : localize("contribute-review-changes")} - -
-
-
-
+ ))}
)} diff --git a/web/src/pages/projects/index.tsx b/web/src/pages/projects/index.tsx index ade771947..51874cf4b 100644 --- a/web/src/pages/projects/index.tsx +++ b/web/src/pages/projects/index.tsx @@ -1,13 +1,12 @@ import React from "react"; import { useEffect } from "react"; import { Helmet } from "react-helmet-async"; -import { Link } from "src/components/link"; import { Loading } from "src/components/loading"; import { Locale, useLocale } from "src/components/locale"; import { TryAgain } from "src/components/try-again"; import { fetchProjectsListAction } from "src/redux/actions/projects"; import { useAppDispatch, useAppSelector } from "src/redux/store"; -import { getProjectURL } from "src/utils/project"; +import ProjectCard from "./project-card"; // ts-prune-ignore-next export default function Page(): JSX.Element { @@ -43,70 +42,7 @@ export default function Page(): JSX.Element { ) : (
{projectsList.map((project, projectIndex) => ( - -

{project.name}

-
-
-
- - - - {project.totalRepoStars} -
-
-
- - - - {project.totalRepoScore} -
-
-
- - - - {project.totalRepoContributorCount} -
-
- + ))}
)} diff --git a/web/src/pages/projects/project-card/index.tsx b/web/src/pages/projects/project-card/index.tsx new file mode 100644 index 000000000..7e8dd52dd --- /dev/null +++ b/web/src/pages/projects/project-card/index.tsx @@ -0,0 +1,84 @@ +import { ProjectEntity } from "@dzcode.io/models/dist/project"; +import React from "react"; +import { Link } from "src/components/link"; +import { getProjectURL } from "src/utils/project"; + +export default function ProjectCard({ + key, + project, +}: { + key: React.Key; + project: Pick & { + totalRepoContributorCount: number; + totalRepoScore: number; + totalRepoStars: number; + ranking: number; + }; +}) { + return ( + +

{project.name}

+
+
+
+ + + + {project.totalRepoStars} +
+
+
+ + + + {project.totalRepoScore} +
+
+
+ + + + {project.totalRepoContributorCount} +
+
+ + ); +} diff --git a/web/src/pages/team/contributor-card/index.tsx b/web/src/pages/team/contributor-card/index.tsx new file mode 100644 index 000000000..3dbc362ec --- /dev/null +++ b/web/src/pages/team/contributor-card/index.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { ContributorEntity } from "@dzcode.io/models/dist/contributor"; +import { Link } from "src/components/link"; +import { getContributorURL } from "src/utils/contributor"; + +export default function ContributorCard({ + contributor, + key, +}: { + key: React.Key; + contributor: Pick & { + ranking: number; + totalContributionScore: number; + totalRepositoryCount: number; + }; +}) { + return ( + + {contributor.name} +
+
+ {contributor.name} + {contributor.name} +
+
+
+ + + + + {contributor.totalContributionScore} +
+
+
+ {contributor.totalRepositoryCount} + + + + + +
+
+
+ + ); +} diff --git a/web/src/pages/team/index.tsx b/web/src/pages/team/index.tsx index 042c304ab..4781f9350 100644 --- a/web/src/pages/team/index.tsx +++ b/web/src/pages/team/index.tsx @@ -1,13 +1,12 @@ import React from "react"; import { useEffect } from "react"; import { Helmet } from "react-helmet-async"; -import { Link } from "src/components/link"; import { Loading } from "src/components/loading"; import { Locale, useLocale } from "src/components/locale"; import { TryAgain } from "src/components/try-again"; import { fetchContributorsListAction } from "src/redux/actions/contributors"; import { useAppDispatch, useAppSelector } from "src/redux/store"; -import { getContributorURL } from "src/utils/contributor"; +import ContributorCard from "./contributor-card"; // ts-prune-ignore-next export default function Page(): JSX.Element { @@ -44,71 +43,7 @@ export default function Page(): JSX.Element { ) : (
{contributorsList.map((contributor, contributorIndex) => ( - - {contributor.name} -
-
- {contributor.name} - {contributor.name} -
-
-
- - - - - {contributor.totalContributionScore} -
-
-
- {contributor.totalRepositoryCount} - - - - - -
-
-
- + ))}
)} From 284361b43b28229cf4769be0f1ddd20f731c6805 Mon Sep 17 00:00:00 2001 From: omdxp Date: Tue, 24 Dec 2024 09:53:43 +0100 Subject: [PATCH 03/19] feat: enhance search functionality and update contribution types for better data retrieval --- api/src/app/endpoints.ts | 4 + api/src/contribution/types.ts | 19 +-- api/src/contributor/types.ts | 14 +- api/src/project/types.ts | 16 +-- api/src/search/service.ts | 2 +- web/src/_entry/app.tsx | 2 +- web/src/components/search.tsx | 115 ++++++++++------ .../contribute/contribute-card/index.tsx | 41 +++--- web/src/pages/projects/project-card/index.tsx | 130 +++++++++--------- web/src/pages/team/contributor-card/index.tsx | 101 +++++++------- web/src/utils/search.ts | 45 ++++++ 11 files changed, 291 insertions(+), 198 deletions(-) create mode 100644 web/src/utils/search.ts diff --git a/api/src/app/endpoints.ts b/api/src/app/endpoints.ts index f1aa3db02..d3dd6cdb3 100644 --- a/api/src/app/endpoints.ts +++ b/api/src/app/endpoints.ts @@ -12,6 +12,7 @@ import { GetProjectsForSitemapResponse, GetProjectsResponse, } from "src/project/types"; +import { SearchResponse } from "src/search/types"; // ts-prune-ignore-next export interface Endpoints { @@ -50,4 +51,7 @@ export interface Endpoints { "api:MileStones/dzcode": { response: GetMilestonesResponse; }; + "api:Search": { + response: SearchResponse; + }; } diff --git a/api/src/contribution/types.ts b/api/src/contribution/types.ts index 8c9f54c2e..531498efb 100644 --- a/api/src/contribution/types.ts +++ b/api/src/contribution/types.ts @@ -4,13 +4,16 @@ import { ProjectEntity } from "@dzcode.io/models/dist/project"; import { RepositoryEntity } from "@dzcode.io/models/dist/repository"; import { GeneralResponse } from "src/app/types"; +export type Contribution = Pick< + ContributionEntity, + "id" | "title" | "type" | "url" | "updatedAt" | "activityCount" +> & { + repository: Pick & { + project: Pick; + }; + contributor: Pick; +}; + export interface GetContributionsResponse extends GeneralResponse { - contributions: Array< - Pick & { - repository: Pick & { - project: Pick; - }; - contributor: Pick; - } - >; + contributions: Array; } diff --git a/api/src/contributor/types.ts b/api/src/contributor/types.ts index 09aac0425..129775d70 100644 --- a/api/src/contributor/types.ts +++ b/api/src/contributor/types.ts @@ -7,14 +7,14 @@ export interface GetContributorsForSitemapResponse extends GeneralResponse { contributors: Array>; } +export type Contributor = Pick & { + ranking: number; + totalContributionScore: number; + totalRepositoryCount: number; +}; + export interface GetContributorsResponse extends GeneralResponse { - contributors: Array< - Pick & { - ranking: number; - totalContributionScore: number; - totalRepositoryCount: number; - } - >; + contributors: Array; } export interface GetContributorResponse extends GeneralResponse { diff --git a/api/src/project/types.ts b/api/src/project/types.ts index 20a453b2b..83962293e 100644 --- a/api/src/project/types.ts +++ b/api/src/project/types.ts @@ -8,15 +8,15 @@ export interface GetProjectsForSitemapResponse extends GeneralResponse { projects: Array>; } +export type Project = Pick & { + totalRepoContributorCount: number; + totalRepoScore: number; + totalRepoStars: number; + ranking: number; +}; + export interface GetProjectsResponse extends GeneralResponse { - projects: Array< - Pick & { - totalRepoContributorCount: number; - totalRepoScore: number; - totalRepoStars: number; - ranking: number; - } - >; + projects: Array; } export interface GetProjectResponse extends GeneralResponse { diff --git a/api/src/search/service.ts b/api/src/search/service.ts index b72b9acbb..b2156b32e 100644 --- a/api/src/search/service.ts +++ b/api/src/search/service.ts @@ -34,7 +34,7 @@ export class SearchService { indexUid: "contribution", q, limit, - attributesToRetrieve: ["id", "title", "type", "activityCount"], + attributesToRetrieve: ["id", "title", "type", "activityCount", "url"], }, { indexUid: "contributor", q, limit, attributesToRetrieve: ["id", "name", "avatarUrl"] }, ], diff --git a/web/src/_entry/app.tsx b/web/src/_entry/app.tsx index 3b55c9398..3b32dd140 100644 --- a/web/src/_entry/app.tsx +++ b/web/src/_entry/app.tsx @@ -120,7 +120,7 @@ const App = () => { const onKeyDown = useCallback((event: KeyboardEvent) => { if (event.key === "/") { event.preventDefault(); - (document.getElementById("search-modal") as HTMLDialogElement).showModal(); + (document.getElementById("search-modal") as HTMLDialogElement)?.showModal(); } }, []); diff --git a/web/src/components/search.tsx b/web/src/components/search.tsx index bab7826b4..b7cb3f915 100644 --- a/web/src/components/search.tsx +++ b/web/src/components/search.tsx @@ -1,22 +1,55 @@ -import React from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { useLocale } from "./locale"; -import { useAppSelector } from "src/redux/store"; import ProjectCard from "src/pages/projects/project-card"; import ContributorCard from "src/pages/team/contributor-card"; import ContributionCard from "src/pages/contribute/contribute-card"; +import { useSearch } from "src/utils/search"; +import { Project } from "@dzcode.io/api/dist/project/types"; +import { Contribution } from "@dzcode.io/api/dist/contribution/types"; +import { Contributor } from "@dzcode.io/api/dist/contributor/types"; export function Search(): JSX.Element { const { localize } = useLocale(); - // @TODO-OB: use the search results from the API call (these are just placeholders) - const { projectsList } = useAppSelector((state) => state.projectsPage); - const { contributorsList } = useAppSelector((state) => state.contributorsPage); - const { contributionsList } = useAppSelector((state) => state.contributionsPage); + const [query, setQuery] = useState(""); + + const results = useSearch(query); + + const hideModal = useCallback(() => { + (document.getElementById("search-modal") as HTMLDialogElement).hidePopover(); + }, []); + + const onSearchInputChange = useCallback((e: React.ChangeEvent) => { + setQuery(e.target.value); + }, []); + + const projectsList = useMemo(() => { + return (results?.searchResults.results || []) + .filter((result) => result.indexUid === "project") + .flatMap((projects) => projects.hits) as unknown as Array; + }, [results?.searchResults.results]); + + const contributorsList = useMemo(() => { + return (results?.searchResults.results || []) + .filter((result) => result.indexUid === "contributor") + .flatMap((contributors) => contributors.hits) as unknown as Array; + }, [results?.searchResults.results]); + + const contributionsList = useMemo(() => { + return (results?.searchResults.results || []) + .filter((result) => result.indexUid === "contribution") + .flatMap((contributions) => contributions.hits) as unknown as Array; + }, [results?.searchResults.results]); return ( - -
-