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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 323 additions & 0 deletions
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;
Lines changed: 47 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy