Skip to content

Commit c22b636

Browse files
committed
Create new plugin page at /plugins-new
1 parent 71e9d61 commit c22b636

File tree

3 files changed

+371
-0
lines changed

3 files changed

+371
-0
lines changed

src/css/docs.css

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
/* Card */
4848
--ifm-card-background-color: #f6f8fa;
4949
--ifm-card-border-radius: var(--ifm-global-radius);
50+
--ifm-global-shadow-lw: 0 1px 15px 0 rgba(0, 0, 0, 0.1);
5051
}
5152

5253
/* Element defaults */

src/pages/plugins-new/index.js

+323
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
import React, {useEffect, useState} from 'react';
2+
import classnames from 'classnames';
3+
4+
import Layout from '@theme/Layout';
5+
6+
import styles from './plugins.module.scss';
7+
8+
const baseUrl = `https://registry.npmjs.com/-/v1/search`;
9+
10+
const internalKeywords = ['gulp', 'gulpplugin'];
11+
12+
function isInternalKeyword(keyword) {
13+
return !internalKeywords.includes(keyword);
14+
}
15+
16+
class Plugin {
17+
constructor(object) {
18+
this._package = object.package;
19+
}
20+
21+
get key() {
22+
return this._package.name;
23+
}
24+
25+
get name() {
26+
return this._package.name;
27+
}
28+
29+
get description() {
30+
return this._package.description;
31+
}
32+
33+
get version() {
34+
return `v${this._package.version}`;
35+
}
36+
37+
get keywords() {
38+
// Unique Keywords
39+
const keywords = new Set(this._package.keywords);
40+
return Array.from(keywords).filter(isInternalKeyword);
41+
}
42+
43+
get primaryUrl() {
44+
const { npm, homepage, repository } = this._package.links;
45+
46+
if (npm) {
47+
return npm;
48+
}
49+
50+
if (homepage) {
51+
return homepage;
52+
}
53+
54+
return repository;
55+
}
56+
57+
get links() {
58+
const { npm, homepage, repository } = this._package.links;
59+
const links = [];
60+
if (npm) {
61+
links.push({ text: 'npm', href: npm });
62+
}
63+
64+
if (homepage &&
65+
homepage !== npm &&
66+
homepage !== repository &&
67+
homepage !== `${repository}#readme`) {
68+
links.push({ text: 'homepage', href: homepage });
69+
}
70+
71+
if (repository) {
72+
links.push({ text: 'repository', href: repository });
73+
}
74+
75+
return links;
76+
}
77+
}
78+
79+
function toPlugin(object) {
80+
return new Plugin(object);
81+
}
82+
83+
function PluginFooter({ keywords = [] }) {
84+
if (keywords.length === 0) {
85+
return null;
86+
} else {
87+
return (
88+
<div className="card__footer">
89+
<ul className={classnames('pills padding-top--sm text--normal text--right', styles.pluginCardKeywords)}>
90+
{keywords.map((keyword) => <li key={keyword} className="pill-item">{keyword}</li>)}
91+
</ul>
92+
</div>
93+
);
94+
}
95+
}
96+
97+
function PluginComponent({ plugin }) {
98+
return (
99+
<div className="row padding-vert--md">
100+
<div className="col col--10 col--offset-1">
101+
<div key={plugin.key} className="card">
102+
<div className={classnames('card__header', styles.pluginCardHeader)}>
103+
<h2><a className={styles.primaryUrl} href={plugin.primaryUrl}>{plugin.name}</a></h2>
104+
<span className="badge badge--primary">{plugin.version}</span>
105+
</div>
106+
<div className="card__body">
107+
{plugin.description}
108+
<div className="padding-top--sm">
109+
{plugin.links.map((link) => <a key={link.text} className="padding-right--sm" href={link.href}>{link.text}</a>)}
110+
</div>
111+
</div>
112+
<PluginFooter keywords={plugin.keywords} />
113+
</div>
114+
</div>
115+
</div>
116+
)
117+
}
118+
119+
function noop(evt) {
120+
evt && evt.preventDefault();
121+
}
122+
123+
function Paginate({onPage = noop}) {
124+
return (
125+
<div className="row padding-vert--md">
126+
<div className="col col--4 col--offset-4">
127+
<button className="button button--block button--primary" onClick={onPage}>
128+
Load more
129+
</button>
130+
</div>
131+
</div>
132+
);
133+
}
134+
135+
function keywordsToQuerystring(keywords) {
136+
let keywordsStr = `keywords:`;
137+
138+
if (keywords.size) {
139+
keywordsStr += [...keywords].join(',');
140+
keywordsStr += `+gulpplugin`;
141+
} else {
142+
keywordsStr += `gulpplugin`;
143+
}
144+
145+
return keywordsStr;
146+
}
147+
148+
async function fetchPackages(keywords, searchText = '', pageNumber = 0) {
149+
const pageSize = 20;
150+
151+
let search = [
152+
keywordsToQuerystring(keywords),
153+
];
154+
if (searchText) {
155+
search.push(encodeURIComponent(searchText));
156+
}
157+
158+
const from = `${pageNumber * pageSize}`;
159+
const text = search.join(' ');
160+
161+
try {
162+
const initialUrl = `${baseUrl}?from=${from}&text=${text}`;
163+
const response = await fetch(initialUrl);
164+
const { total, objects } = await response.json();
165+
return { total, plugins: objects.map(toPlugin) };
166+
} catch(err) {
167+
console.log(err);
168+
return { total: 0, plugins: [] };
169+
}
170+
}
171+
172+
function keywordsRegExp() {
173+
return /keywords:([\S]*)\s*/g;
174+
}
175+
176+
function extractKeywords(text) {
177+
// This is really messy, should probably test it
178+
let newKeywords = new Set();
179+
for (const match of text.matchAll(keywordsRegExp())) {
180+
const kw = match[1].split(`,`);
181+
newKeywords = new Set([...newKeywords, ...kw]);
182+
}
183+
184+
let newText = text.replace(keywordsRegExp(), ``).trim();
185+
186+
return [newKeywords, newText];
187+
}
188+
189+
function formatSearch(keywords = (new Set()), searchText = '') {
190+
let searchQuery = [];
191+
if (keywords.size) {
192+
searchQuery.push(`keywords:${[...keywords].join(',')}`);
193+
}
194+
if (searchText) {
195+
searchQuery.push(searchText);
196+
}
197+
198+
return searchQuery.join(' ');
199+
}
200+
201+
function useSearch() {
202+
const [isPopular, setIsPopular] = useState(true);
203+
const [title, setTitle] = useState(``);
204+
const [plugins, setPlugins] = useState([]);
205+
const [placeholder, setPlaceholder] = useState(`Search`);
206+
const [keywords, setKeywords] = useState(new Set());
207+
const [searchText, setSearchText] = useState(``);
208+
// Pagination stuff
209+
const [isPaging, setIsPaging] = useState(false);
210+
const [pageNumber, setPageNumer] = useState(0);
211+
const [hasMore, setHasMore] = useState(false);
212+
213+
useEffect(() => {
214+
fetchPackages(keywords, searchText, pageNumber)
215+
.then((results) => {
216+
if (isPopular) {
217+
setTitle(`Popular plugins`);
218+
setPlaceholder(`Search ${results.total} plugins`);
219+
} else {
220+
const searchQuery = formatSearch(keywords, searchText);
221+
setTitle(`Found ${results.total} searching for \`${searchQuery}\``);
222+
}
223+
224+
let loadedPlugins;
225+
if (isPaging) {
226+
loadedPlugins = plugins.concat(results.plugins);
227+
} else {
228+
loadedPlugins = results.plugins;
229+
}
230+
231+
if (loadedPlugins.length === results.total) {
232+
setHasMore(false);
233+
} else {
234+
setHasMore(true);
235+
}
236+
237+
setPlugins(loadedPlugins);
238+
});
239+
}, [searchText, keywords, pageNumber]);
240+
241+
function search(text) {
242+
// Undo paging upon search
243+
setIsPaging(false);
244+
setPageNumer(0);
245+
246+
// Empty search reverts to popular
247+
if (text === ``) {
248+
setIsPopular(true);
249+
} else {
250+
setIsPopular(false);
251+
}
252+
253+
const [newKeywords, newText] = extractKeywords(text);
254+
setKeywords(newKeywords);
255+
setSearchText(newText);
256+
}
257+
258+
function paginate() {
259+
setIsPaging(true);
260+
setPageNumer(pageNumber + 1);
261+
}
262+
263+
const state = {
264+
title,
265+
plugins,
266+
placeholder,
267+
hasMore,
268+
};
269+
270+
const handlers = {
271+
search,
272+
paginate,
273+
};
274+
275+
return [state, handlers];
276+
}
277+
278+
279+
function PluginsPage() {
280+
const [{title, plugins, placeholder, hasMore}, {search, paginate}] = useSearch();
281+
const [searchInput, setSearchInput] = useState(``);
282+
283+
let onSubmit = (evt) => {
284+
evt.preventDefault();
285+
search(searchInput);
286+
};
287+
288+
let onChange = (evt) => {
289+
evt.preventDefault();
290+
setSearchInput(evt.target.value);
291+
};
292+
293+
return (
294+
<Layout title="Plugins">
295+
<main className="container padding-vert--lg">
296+
<div className="row">
297+
<div className="col col--10 col--offset-1">
298+
<form className={styles.searchContainer} onSubmit={onSubmit}>
299+
<input
300+
type="search"
301+
className={styles.searchInput}
302+
placeholder={placeholder}
303+
value={searchInput}
304+
onChange={onChange} />
305+
<button className="button button--lg button--primary">Search</button>
306+
</form>
307+
</div>
308+
</div>
309+
<div className="row">
310+
<div className="col col--10 col--offset-1">
311+
<h1 className="margin-vert--md">{title}</h1>
312+
</div>
313+
</div>
314+
{plugins.map((plugin) => (
315+
<PluginComponent key={plugin.name} plugin={plugin} />
316+
))}
317+
{hasMore ? <Paginate onPage={paginate} /> : null}
318+
</main>
319+
</Layout>
320+
);
321+
}
322+
323+
export default PluginsPage;
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
.searchContainer {
2+
// I hate grid utilities
3+
display: flex !important;
4+
}
5+
6+
.searchInput {
7+
flex-grow: 1;
8+
9+
appearance: none;
10+
background-color: var(--ifm-navbar-search-input-background-color);
11+
background-image: var(--ifm-navbar-search-input-icon);
12+
background-position-x: 0.75rem;
13+
background-position-y: center;
14+
background-repeat: no-repeat;
15+
background-size: 1rem 1rem;
16+
border-radius: var(--ifm-global-radius);
17+
border-width: 0;
18+
cursor: text;
19+
color: var(--ifm-navbar-search-input-color);
20+
display: inline-block;
21+
font-size: 1.1rem;
22+
line-height: 3rem;
23+
outline: none;
24+
padding: 0 0.5rem 0 2.25rem;
25+
26+
&::placeholder {
27+
color: var(--ifm-navbar-search-input-placeholder-color);
28+
}
29+
}
30+
31+
.pluginCardHeader {
32+
display: flex;
33+
justify-content: space-between;
34+
align-items: center;
35+
36+
h2 {
37+
margin-bottom: 0;
38+
}
39+
}
40+
41+
.pluginCardKeywords {
42+
border-top: 1px solid #f4f4f4;
43+
}
44+
45+
.primaryUrl {
46+
color: var(--ifm-font-base-color);
47+
}

0 commit comments

Comments
 (0)