Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit94cf03c

Browse files
authored
Merge pull request#159 from barrymun/feature/search
Feature - Implement basic search functionality v2
2 parentsdf7dcbf +c0b3bff commit94cf03c

23 files changed

+1079
-321
lines changed

‎package-lock.json‎

Lines changed: 125 additions & 101 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"prismjs":"^1.29.0",
2424
"react":"^18.3.1",
2525
"react-dom":"^18.3.1",
26-
"react-router-dom":"^6.27.0",
26+
"react-router-dom":"^7.1.1",
2727
"react-syntax-highlighter":"^15.6.1"
2828
},
2929
"devDependencies": {

‎src/AppRouter.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import{Route,Routes}from"react-router-dom";
2+
3+
importAppfrom"@components/App";
4+
importSnippetListfrom"@components/SnippetList";
5+
6+
constAppRouter=()=>{
7+
return(
8+
<Routes>
9+
<Routeelement={<App/>}>
10+
<Routepath="/"element={<SnippetList/>}/>
11+
<Routepath="/:languageName"element={<SnippetList/>}/>
12+
<Route
13+
path="/:languageName/:subLanguageName/:categoryName"
14+
element={<SnippetList/>}
15+
/>
16+
</Route>
17+
</Routes>
18+
);
19+
};
20+
21+
exportdefaultAppRouter;

‎src/components/App.tsx‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import{FC}from"react";
2+
3+
import{AppProvider}from"@contexts/AppContext";
4+
5+
importContainerfrom"./Container";
6+
7+
interfaceAppProps{}
8+
9+
constApp:FC<AppProps>=()=>{
10+
return(
11+
<AppProvider>
12+
<Container/>
13+
</AppProvider>
14+
);
15+
};
16+
17+
exportdefaultApp;

‎src/components/CategoryList.tsx‎

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,58 @@
1-
import{useEffect}from"react";
1+
import{FC}from"react";
2+
import{useNavigate,useSearchParams}from"react-router-dom";
23

34
import{useAppContext}from"@contexts/AppContext";
45
import{useCategories}from"@hooks/useCategories";
6+
import{defaultCategoryName}from"@utils/consts";
7+
import{slugify}from"@utils/slugify";
8+
9+
interfaceCategoryListItemProps{
10+
name:string;
11+
}
12+
13+
constCategoryListItem:FC<CategoryListItemProps>=({ name})=>{
14+
constnavigate=useNavigate();
15+
const[searchParams]=useSearchParams();
16+
17+
const{ language, subLanguage, category}=useAppContext();
18+
19+
consthandleSelect=()=>{
20+
navigate({
21+
pathname:`/${slugify(language.name)}/${slugify(subLanguage)}/${slugify(name)}`,
22+
search:searchParams.toString(),
23+
});
24+
};
25+
26+
return(
27+
<liclassName="category">
28+
<button
29+
className={`category__btn${
30+
slugify(name)===slugify(category) ?"category__btn--active" :""
31+
}`}
32+
onClick={handleSelect}
33+
>
34+
{name}
35+
</button>
36+
</li>
37+
);
38+
};
539

640
constCategoryList=()=>{
7-
const{ category, setCategory}=useAppContext();
841
const{ fetchedCategories, loading, error}=useCategories();
942

10-
useEffect(()=>{
11-
setCategory(fetchedCategories[0]);
12-
},[setCategory,fetchedCategories]);
13-
14-
if(loading)return<div>Loading...</div>;
43+
if(loading){
44+
return<div>Loading...</div>;
45+
}
1546

16-
if(error)return<div>Error occurred:{error}</div>;
47+
if(error){
48+
return<div>Error occurred:{error}</div>;
49+
}
1750

1851
return(
1952
<ulrole="list"className="categories">
53+
<CategoryListItemname={defaultCategoryName}/>
2054
{fetchedCategories.map((name,idx)=>(
21-
<likey={idx}className="category">
22-
<button
23-
className={`category__btn${
24-
name===category ?"category__btn--active" :""
25-
}`}
26-
onClick={()=>setCategory(name)}
27-
>
28-
{name}
29-
</button>
30-
</li>
55+
<CategoryListItemkey={idx}name={name}/>
3156
))}
3257
</ul>
3358
);

‎src/App.tsx‎renamed to ‎src/components/Container.tsx‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
importSnippetListfrom"@components/SnippetList";
1+
import{FC}from"react";
2+
import{Outlet}from"react-router-dom";
3+
24
import{useAppContext}from"@contexts/AppContext";
35
importBannerfrom"@layouts/Banner";
46
importFooterfrom"@layouts/Footer";
57
importHeaderfrom"@layouts/Header";
68
importSidebarfrom"@layouts/Sidebar";
79

8-
constApp=()=>{
10+
interfaceContainerProps{}
11+
12+
constContainer:FC<ContainerProps>=()=>{
913
const{ category}=useAppContext();
1014

1115
return(
@@ -18,12 +22,12 @@ const App = () => {
1822
<h2className="section-title">
1923
{category ?category :"Select a category"}
2024
</h2>
21-
<SnippetList/>
25+
<Outlet/>
2226
</section>
2327
</main>
2428
<Footer/>
2529
</div>
2630
);
2731
};
2832

29-
exportdefaultApp;
33+
exportdefaultContainer;

‎src/components/LanguageSelector.tsx‎

Lines changed: 116 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,127 @@
1+
/**
2+
* Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
3+
*/
4+
15
import{useRef,useEffect,useState,useMemo}from"react";
6+
import{useNavigate}from"react-router-dom";
27

38
import{useAppContext}from"@contexts/AppContext";
49
import{useKeyboardNavigation}from"@hooks/useKeyboardNavigation";
510
import{useLanguages}from"@hooks/useLanguages";
611
import{LanguageType}from"@types";
12+
import{configureUserSelection}from"@utils/configureUserSelection";
13+
import{
14+
getLanguageDisplayLogo,
15+
getLanguageDisplayName,
16+
}from"@utils/languageUtils";
17+
import{slugify}from"@utils/slugify";
718

819
importSubLanguageSelectorfrom"./SubLanguageSelector";
920

10-
// Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
11-
1221
constLanguageSelector=()=>{
13-
const{ language, setLanguage}=useAppContext();
22+
constnavigate=useNavigate();
23+
24+
const{ language, subLanguage, setSearchText}=useAppContext();
1425
const{ fetchedLanguages, loading, error}=useLanguages();
15-
constallLanguages=useMemo(
16-
()=>
17-
fetchedLanguages.flatMap((lang)=>
18-
lang.subLanguages.length>0
19-
?[
20-
lang,
21-
...lang.subLanguages.map((subLang)=>({
22-
...subLang,
23-
mainLanguage:lang,
24-
subLanguages:[],
25-
})),
26-
]
27-
:[lang]
28-
),
29-
[fetchedLanguages]
30-
);
3126

3227
constdropdownRef=useRef<HTMLDivElement>(null);
33-
const[isOpen,setIsOpen]=useState(false);
28+
const[isOpen,setIsOpen]=useState<boolean>(false);
3429
const[openedLanguages,setOpenedLanguages]=useState<LanguageType[]>([]);
3530

36-
consthandleSelect=(selected:LanguageType)=>{
37-
setLanguage(selected);
31+
constkeyboardItems=useMemo(()=>{
32+
returnfetchedLanguages.flatMap((lang)=>
33+
openedLanguages.map((ol)=>ol.name).includes(lang.name)
34+
?[
35+
{languageName:lang.name},
36+
...lang.subLanguages.map((sl)=>({
37+
languageName:lang.name,
38+
subLanguageName:sl.name,
39+
})),
40+
]
41+
:[{languageName:lang.name}]
42+
);
43+
},[fetchedLanguages,openedLanguages]);
44+
45+
constdisplayName=useMemo(
46+
()=>getLanguageDisplayName(language.name,subLanguage),
47+
[language.name,subLanguage]
48+
);
49+
50+
constdisplayLogo=useMemo(
51+
()=>getLanguageDisplayLogo(language.name,subLanguage),
52+
[language.name,subLanguage]
53+
);
54+
55+
consthandleToggleSubLanguage=(name:LanguageType["name"])=>{
56+
constisAlreadyOpened=openedLanguages.some((lang)=>lang.name===name);
57+
constopenedLang=fetchedLanguages.find((lang)=>lang.name===name);
58+
if(openedLang===undefined||openedLang.subLanguages.length===0){
59+
return;
60+
}
61+
62+
if(!isAlreadyOpened){
63+
setOpenedLanguages((prev)=>[...prev,openedLang]);
64+
}else{
65+
setOpenedLanguages((prev)=>
66+
prev.filter((lang)=>lang.name!==openedLang.name)
67+
);
68+
}
69+
};
70+
71+
/**
72+
* When setting a new language we need to ensure that a category
73+
* has been set given this new language.
74+
* Ensure that the search text is cleared.
75+
*/
76+
consthandleSelect=async(selected:LanguageType)=>{
77+
const{
78+
language:newLanguage,
79+
subLanguage:newSubLanguage,
80+
category:newCategory,
81+
}=awaitconfigureUserSelection({
82+
languageName:selected.name,
83+
});
84+
85+
setSearchText("");
86+
navigate(
87+
`/${slugify(newLanguage.name)}/${slugify(newSubLanguage)}/${slugify(newCategory)}`
88+
);
3889
setIsOpen(false);
3990
setOpenedLanguages([]);
4091
};
4192

93+
constafterSelect=()=>{
94+
setIsOpen(false);
95+
};
96+
97+
consthandleSubLanguageSelect=async(
98+
selectedLanguageName:LanguageType["name"],
99+
selectedSubLanguageName:
100+
|LanguageType["subLanguages"][number]["name"]
101+
|undefined
102+
)=>{
103+
const{
104+
language:newLanguage,
105+
subLanguage:newSubLanguage,
106+
category:newCategory,
107+
}=awaitconfigureUserSelection({
108+
languageName:selectedLanguageName,
109+
subLanguageName:selectedSubLanguageName,
110+
});
111+
112+
setSearchText("");
113+
navigate(
114+
`/${slugify(newLanguage.name)}/${slugify(newSubLanguage)}/${slugify(newCategory)}`
115+
);
116+
afterSelect();
117+
};
118+
42119
const{ focusedIndex, handleKeyDown, resetFocus, focusFirst}=
43120
useKeyboardNavigation({
44-
items:allLanguages,
121+
items:keyboardItems,
45122
isOpen,
46-
openedLanguages,
47-
toggleDropdown:(openedLang)=>handleToggleSublanguage(openedLang),
48-
onSelect:handleSelect,
123+
toggleDropdown:(l)=>handleToggleSubLanguage(l),
124+
onSelect:(l,sl)=>handleSubLanguageSelect(l,sl),
49125
onClose:()=>setIsOpen(false),
50126
});
51127

@@ -60,20 +136,6 @@ const LanguageSelector = () => {
60136
},0);
61137
};
62138

63-
consthandleToggleSublanguage=(openedLang:LanguageType)=>{
64-
constisAlreadyOpened=openedLanguages.some(
65-
(lang)=>lang.name===openedLang.name
66-
);
67-
68-
if(!isAlreadyOpened){
69-
setOpenedLanguages((prev)=>[...prev,openedLang]);
70-
}else{
71-
setOpenedLanguages((prev)=>
72-
prev.filter((lang)=>lang.name!==openedLang.name)
73-
);
74-
}
75-
};
76-
77139
consttoggleDropdown=()=>{
78140
setIsOpen((prev)=>{
79141
if(!prev)setTimeout(focusFirst,0);
@@ -88,13 +150,6 @@ const LanguageSelector = () => {
88150
// eslint-disable-next-line react-hooks/exhaustive-deps
89151
},[isOpen]);
90152

91-
useEffect(()=>{
92-
if(language.mainLanguage){
93-
handleToggleSublanguage(language.mainLanguage);
94-
}
95-
// eslint-disable-next-line react-hooks/exhaustive-deps
96-
},[language]);
97-
98153
useEffect(()=>{
99154
if(isOpen&&focusedIndex>=0){
100155
constelement=document.querySelector(
@@ -104,8 +159,13 @@ const LanguageSelector = () => {
104159
}
105160
},[isOpen,focusedIndex]);
106161

107-
if(loading)return<p>Loading languages...</p>;
108-
if(error)return<p>Error fetching languages:{error}</p>;
162+
if(loading){
163+
return<p>Loading languages...</p>;
164+
}
165+
166+
if(error){
167+
return<p>Error fetching languages:{error}</p>;
168+
}
109169

110170
return(
111171
<div
@@ -121,8 +181,8 @@ const LanguageSelector = () => {
121181
onClick={toggleDropdown}
122182
>
123183
<divclassName="selector__value">
124-
<imgsrc={language.icon}alt=""/>
125-
<span>{language.name||"Select a language"}</span>
184+
<imgsrc={displayLogo}alt=""/>
185+
<span>{displayName}</span>
126186
</div>
127187
<spanclassName="selector__arrow"/>
128188
</button>
@@ -136,13 +196,12 @@ const LanguageSelector = () => {
136196
{fetchedLanguages.map((lang,index)=>
137197
lang.subLanguages.length>0 ?(
138198
<SubLanguageSelector
139-
key={index}
140-
mainLanguage={lang}
141-
afterSelect={()=>{
142-
setIsOpen(false);
143-
}}
199+
key={lang.name}
144200
opened={openedLanguages.includes(lang)}
145-
onDropdownToggle={handleToggleSublanguage}
201+
parentLanguage={lang}
202+
onDropdownToggle={handleToggleSubLanguage}
203+
handleParentSelect={handleSelect}
204+
afterSelect={afterSelect}
146205
/>
147206
) :(
148207
<li

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp