Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e21783e
feat: add search input to top bar with localization support
omdxp Dec 23, 2024
9c939c8
feat: implement search modal and enhance top bar with search function…
omdxp Dec 23, 2024
284361b
feat: enhance search functionality and update contribution types for …
omdxp Dec 24, 2024
926b66b
feat: add localized labels for search contributions, contributors, an…
omdxp Dec 24, 2024
32c87a0
feat: update top bar to include a search button and improve modal dis…
omdxp Dec 24, 2024
aeacf86
refactor: update contribution, contributor, and project types for imp…
omdxp Dec 24, 2024
7e05fb9
refactor: update search component to use entity models for improved t…
omdxp Dec 24, 2024
ec6df46
feat: enhance search component with loading state and localized messa…
omdxp Dec 24, 2024
a20f438
refactor: update import paths for project, contributor, and contribut…
omdxp Dec 24, 2024
622a8cb
feat: add query parameters type to Search endpoint for enhanced reada…
omdxp Dec 24, 2024
c5aa5be
refactor: move keyboard event handling to Search component for better…
omdxp Dec 24, 2024
7693ae4
feat: implement useSearchModal hook for improved modal handling in Se…
omdxp Dec 24, 2024
b4423aa
refactor: update logo import and adjust button sizes in TopBar for im…
omdxp Dec 24, 2024
9769b26
refactor: rename contribute-card to contribution-card and update impo…
omdxp Dec 24, 2024
7132657
refactor: update search component to use typed responses for projects…
omdxp Dec 24, 2024
2917da6
refactor: update TopBar search input to be read-only and enhance clic…
omdxp Dec 24, 2024
0277c2d
refactor: simplify results handling in Search component and initializ…
omdxp Dec 24, 2024
920b065
refactor: move onKeyDown function inside useEffect for better encapsu…
omdxp Dec 24, 2024
3af1e4e
refactor: update ContributorCard and TopBar components for consistenc…
omdxp Dec 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/src/app/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
GetProjectsForSitemapResponse,
GetProjectsResponse,
} from "src/project/types";
import { SearchResponse } from "src/search/types";

// ts-prune-ignore-next
export interface Endpoints {
Expand Down Expand Up @@ -50,4 +51,7 @@ export interface Endpoints {
"api:MileStones/dzcode": {
response: GetMilestonesResponse;
};
"api:Search": {
response: SearchResponse;
};
}
2 changes: 1 addition & 1 deletion api/src/search/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"] },
],
Expand Down
18 changes: 17 additions & 1 deletion web/src/_entry/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down Expand Up @@ -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 (
<>
<div className="flex flex-col min-h-screen">
<Search />
<TopBar version={window.bundleInfo.version} links={topBarLinks} />
<Routes>
{routes.map((route) => {
Expand Down
21 changes: 21 additions & 0 deletions web/src/components/locale/dictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -433,4 +434,24 @@ Besides the open tasks on [/Contribute](/Contribute) page, you can also contribu
en: "Algeria Codes",
ar: "الجزائر تبرمج",
},
"search-contributions": {
en: "Contributions",
ar: "مساهمات",
},
"search-contributors": {
en: "Contributors",
ar: "مساهمين",
},
"search-projects": {
en: "Projects",
ar: "مشاريع",
},
"search-type-to-search": {
en: "Type to search...",
ar: "اكتب للبحث...",
},
"search-no-results-found": {
en: "No results found",
ar: "لم يتم العثور على نتائج",
},
} as const;
169 changes: 169 additions & 0 deletions web/src/components/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useCallback, useMemo, useState } from "react";
import { Locale, useLocale } from "./locale";
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 { ProjectEntity } from "@dzcode.io/models/dist/project";
import { ContributorEntity } from "@dzcode.io/models/dist/contributor";
import { ContributionEntity } from "@dzcode.io/models/dist/contribution";
import { RepositoryEntity } from "@dzcode.io/models/dist/repository";

export function Search(): JSX.Element {
const { localize } = useLocale();
const [query, setQuery] = useState("");

const { results, isFetching } = useSearch(query);

const hideModal = useCallback(() => {
(document.getElementById("search-modal") as HTMLDialogElement).hidePopover();
}, []);

const onSearchInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
}, []);

const projectsList = useMemo(() => {
return (results?.searchResults.results || [])
.filter((result) => result.indexUid === "project")
.flatMap((projects) => projects.hits) as unknown as Array<
Pick<ProjectEntity, "id" | "name"> & {
totalRepoContributorCount: number;
totalRepoScore: number;
totalRepoStars: number;
ranking: number;
}
>;
}, [results?.searchResults.results]);

const contributorsList = useMemo(() => {
return (results?.searchResults.results || [])
.filter((result) => result.indexUid === "contributor")
.flatMap((contributors) => contributors.hits) as unknown as Array<
Pick<ContributorEntity, "id" | "name" | "avatarUrl"> & {
ranking: number;
totalContributionScore: number;
totalRepositoryCount: number;
}
>;
}, [results?.searchResults.results]);

const contributionsList = useMemo(() => {
return (results?.searchResults.results || [])
.filter((result) => result.indexUid === "contribution")
.flatMap((contributions) => contributions.hits) as unknown as Array<
Pick<ContributionEntity, "id" | "title" | "type" | "url" | "updatedAt" | "activityCount"> & {
repository: Pick<RepositoryEntity, "id" | "owner" | "name"> & {
project: Pick<ProjectEntity, "id" | "name">;
};
contributor: Pick<ContributorEntity, "id" | "name" | "username" | "avatarUrl">;
}
>;
}, [results?.searchResults.results]);

const searchTextOutput = useMemo(() => {
if (isFetching) {
return "";
}
if (query === "") {
return localize("search-type-to-search");
}
if (
projectsList.length === 0 &&
contributorsList.length === 0 &&
contributionsList.length === 0
) {
return localize("search-no-results-found");
}
return "";
}, [
isFetching,
query,
projectsList.length,
contributorsList.length,
contributionsList.length,
localize,
]);

return (
<dialog id="search-modal" className="modal">
<div className="modal-box w-11/12 max-w-5xl h-[80vh] flex flex-col">
<label className="input input-bordered flex items-center gap-2 bg-neutral sticky top-0 z-10">
<input
type="text"
className="grow h-12"
placeholder={localize("navbar-section-search")}
onChange={onSearchInputChange}
/>
{isFetching ? (
<span className="loading loading-dots loading-lg"></span>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
className="h-4 w-4 opacity-70"
>
<path
fillRule="evenodd"
d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z"
clipRule="evenodd"
/>
</svg>
)}
</label>
<div className="overflow-y-auto flex-grow">
{searchTextOutput && (
<div className="flex justify-center items-center h-full">
<p>{searchTextOutput}</p>
</div>
)}
{contributionsList?.length > 0 && (
<div className="mt-4">
<h3 className="text-lg font-bold">
<Locale search-contributions />
</h3>
<div className="flex flex-row flex-wrap gap-4 justify-center p-4 max-w-7xl">
{contributionsList.map((contribution) => (
<ContributionCard contribution={contribution} key={contribution.id} compact />
))}
</div>
</div>
)}
{contributorsList?.length > 0 && (
<div className="mt-4">
<h3 className="text-lg font-bold">
<Locale search-contributors />
</h3>
<div className="flex flex-row flex-wrap gap-4 justify-between p-4 max-w-7xl">
{contributorsList.map((contributor) => (
<ContributorCard
contributor={contributor}
key={contributor.id}
compact
onClick={hideModal}
/>
))}
</div>
</div>
)}
{projectsList?.length > 0 && (
<div className="mt-4">
<h3 className="text-lg font-bold">
<Locale search-projects />
</h3>
<div className="flex flex-row flex-wrap gap-4 justify-between p-4 max-w-7xl">
{projectsList.map((project) => (
<ProjectCard project={project} key={project.id} compact onClick={hideModal} />
))}
</div>
</div>
)}
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
);
}
35 changes: 33 additions & 2 deletions web/src/components/top-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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";
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";
Expand Down Expand Up @@ -37,6 +37,12 @@ export function TopBar({ version, links }: TopBarProps): JSX.Element {
return { selectedLanguage, languageOptions };
}, [selectedLanguageCode]);

const { localize } = useLocale();

const showModal = useCallback(() => {
(document.getElementById("search-modal") as HTMLDialogElement)?.showModal();
}, []);

return (
<div className="bg-neutral">
<div className="m-auto flex max-w-7xl flex-row gap-4 p-4">
Expand All @@ -51,6 +57,31 @@ export function TopBar({ version, links }: TopBarProps): JSX.Element {
{version}
</Link>
<div className="flex-1" />
<button className="btn btn-ghost btn-circle lg:hidden" onClick={showModal}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</button>
<label className="input input-bordered hidden lg:flex items-center gap-2">
<input
type="text"
className="grow"
placeholder={localize("navbar-section-search")}
onClick={showModal}
/>
<kbd className="kbd kbd-sm">/</kbd>
</label>
<div className="dropdown dropdown-end">
<div tabIndex={0} role="button">
{selectedLanguage.label}
Expand Down
Loading
Loading