Skip to content

Commit 4c96a9f

Browse files
authored
Merge pull request #75 from HusseinSerag/FEATURE-plugins-page
feature: implement barebones of plugins page
2 parents 2c9fb22 + 2c11918 commit 4c96a9f

File tree

8 files changed

+190
-32
lines changed

8 files changed

+190
-32
lines changed

src/components/Plugins/PluginCard.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ExternalLink } from "lucide-react";
2+
import { Plugin } from "../../interfaces/Plugin";
3+
4+
type PluginCardProps = {
5+
plugin: Plugin;
6+
}
7+
export function PluginCard({ plugin }: PluginCardProps) {
8+
return <section className="border gap-4 flex flex-col max-w-[314px] hover:scale-[1.01] p-4 pt-6 m-auto w-full border-card-border rounded-md h-[300px]">
9+
<div className="flex-1 flex flex-col gap-4">
10+
<div>
11+
<div>
12+
<h1 className="text-white text-[22px] font-semibold">{plugin.name}</h1>
13+
{/*
14+
// TODO: should add and fetch data about user using the userId or a github handle linked to it
15+
*/}
16+
</div>
17+
</div>
18+
<div className="text-white text-sm">
19+
{plugin.description}
20+
</div>
21+
<div className="w-full max-w-[200px]">
22+
<img className="w-full" src={plugin.imageURL} />
23+
</div>
24+
</div>
25+
<div className="flex justify-between gap-3 sm:gap-1 flex-col sm:flex-row">
26+
<button className="bg-slate-900 text-[12px] px-4 pt-[6px] pb-[7px] rounded-md text-white">
27+
Read more
28+
</button>
29+
30+
<button className="bg-brand-purple text-[12px] flex justify-center items-center gap-1 px-4 pt-[6px] pb-[7px] rounded-md text-white"><span>
31+
View in NPM
32+
</span>
33+
<ExternalLink className="w-4" /></button>
34+
</div>
35+
36+
</section>
37+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Search } from "lucide-react";
2+
import { useSearchParams } from "react-router-dom";
3+
4+
export default function PluginSearchBar() {
5+
const [searchParams, setSearchParams] = useSearchParams()
6+
const searchQuery = searchParams.get('searchQuery') || "";
7+
const setSearchQuery = (input: string) => {
8+
if(input == ""){
9+
searchParams.delete("searchQuery")
10+
setSearchParams(searchParams)
11+
} else {
12+
searchParams.set("searchQuery",input)
13+
setSearchParams(searchParams)
14+
}
15+
}
16+
return <div>
17+
<div className='bg-slate-900 border focus-within:ring-2 focus-within:ring-slate-300 border-slate-300 flex rounded-md gap-2 px-3 py-2 max-w-[500px] w-full'>
18+
<Search className='text-slate-300 w-4' />
19+
<input value={searchQuery} onChange={(e)=>setSearchQuery(e.target.value)} placeholder='Search plugins...' className='bg-transparent focus:border-0 focus:outline-none focus:ring-0 placeholder:text-sm text-slate-300 rounded-md w-full' />
20+
</div>
21+
</div>
22+
}

src/constants/Endpoints.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// endpoints for making requests
22
const Endpoints = {
33
fetchApiThemes: `${import.meta.env.VITE_GALLERY_API_URL}/api/v1/themes`,
4+
fetchApiPlugins: `${import.meta.env.VITE_GALLERY_API_URL}/api/v1/plugins`,
45
fetchUserProfile: `${import.meta.env.VITE_GALLERY_API_URL}/api/v1/users/profile`,
56
fetchCacheThemes: import.meta.env.VITE_GITHUB_THEMES_CACHE_URL,
67
loginUser: `${import.meta.env.VITE_GALLERY_API_URL}/api/v1/auth/login/process`,

src/hooks/useFetchPlugins.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useEffect, useState } from "react";
2+
import { galleryApiFetch } from "../services/apiService";
3+
import { Plugin } from "../interfaces/Plugin";
4+
5+
/**
6+
* Fetches plugins from the backend api.
7+
*
8+
* @param url url to fetch plugins from
9+
* @param pageSize number of plugins to fetch each page
10+
* @param pageNum page number to fetch for
11+
* @param searchQuery search query for filtering plugins
12+
*/
13+
function useFetchPlugins(
14+
url: string,
15+
pageSize: number,
16+
pageNum: number,
17+
searchQuery?: string
18+
) {
19+
const [plugins, setPlugins] = useState<Plugin[]>([]);
20+
const [isLoading, setIsLoading] = useState(false);
21+
const [error, setError] = useState("");
22+
useEffect(function() {
23+
// avoid race conditions
24+
const controller = new AbortController()
25+
let isAbort = false
26+
27+
setIsLoading(true)
28+
setError("")
29+
setPlugins([])
30+
31+
fetchPlugins(controller, url, searchQuery)
32+
33+
.then((plugins)=> {
34+
setPlugins(plugins)
35+
setError("")
36+
})
37+
38+
.catch((err: Error) => {
39+
if(err.name != 'AbortError') {
40+
setError(err.message);
41+
}
42+
else {
43+
isAbort = true
44+
}
45+
})
46+
47+
.finally(() => {
48+
if(!isAbort) setIsLoading(false)
49+
})
50+
51+
return () => controller.abort()
52+
},[searchQuery, url, pageNum, pageSize])
53+
54+
55+
56+
return {plugins, isLoading, error }
57+
}
58+
59+
const fetchPlugins = async(controller: AbortController, url: string, searchQuery?: string) => {
60+
try{
61+
if(searchQuery){
62+
url += `?searchQuery=${searchQuery}`
63+
}
64+
const data = await galleryApiFetch(url,{
65+
signal: controller.signal
66+
});
67+
const plugins = await data.json()
68+
return plugins.data
69+
} catch(e: unknown){
70+
if((e as Error).name != 'AbortError')
71+
throw new Error("Problem fetching plugins!")
72+
else throw e
73+
}
74+
}
75+
76+
77+
export default useFetchPlugins

src/interfaces/Plugin.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// plugin data fetched from both backend api
2+
export interface Plugin {
3+
id: string;
4+
name: string;
5+
description: string;
6+
favoritesCount: number;
7+
imageURL: string;
8+
createdAt: string;
9+
updatedAt: string;
10+
userId: string;
11+
}

src/pages/LoginProcess.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const LoginProcessPage = () => {
1010
const { setUserData, setIsLoggedIn } = useAuth();
1111
const navigate = useNavigate();
1212
const location = useLocation();
13-
const [gracetime, setgracetime] = useState(false)
13+
const [gracetime, setgracetime] = useState(false)
1414

1515
// Retrieve provider and key from query params to be used for getting user data
1616
const queryParams = new URLSearchParams(location.search);
@@ -28,26 +28,26 @@ const LoginProcessPage = () => {
2828
setUserData(data);
2929
setIsLoggedIn(true);
3030
const redirectUri = localStorage.getItem('login_redirect_uri');
31-
if(gracetime){
32-
//keep waiting until gracetime ends
33-
return;
34-
}
31+
if(gracetime){
32+
//keep waiting until gracetime ends
33+
return;
34+
}
3535
if (redirectUri) {
3636
window.location.href = redirectUri;
3737
} else {
3838
navigate('/themes');
3939
}
4040
}
4141
}, [loading, error, data, gracetime]);
42-
useEffect(()=> {
43-
if(loading){
44-
setgracetime(true)
45-
setTimeout(() => {
46-
//allow the spinner to remove from render once the gracetime elapses
47-
setgracetime(false)
48-
}, SiteConfig.loginSpinnerGraceTime);
49-
}
50-
}, [loading])
42+
useEffect(()=> {
43+
if(loading){
44+
setgracetime(true)
45+
setTimeout(() => {
46+
//allow the spinner to remove from render once the gracetime elapses
47+
setgracetime(false)
48+
}, SiteConfig.loginSpinnerGraceTime);
49+
}
50+
}, [loading])
5151
return (
5252
<div className="h-screen w-full bg-black flex justify-center items-center">
5353
{(loading || gracetime) && <LoadingSpinner />}

src/pages/Plugins.tsx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
import React from 'react';
22

3-
import { Link } from 'react-router-dom';
3+
import { Endpoints } from '../constants/Endpoints';
4+
import useFetchPlugins from '../hooks/useFetchPlugins';
45

6+
import Skeleton from 'react-loading-skeleton';
7+
import { PluginCard } from '../components/Plugins/PluginCard';
8+
import PluginSearchBar from '../components/Plugins/PluginSearchBar';
9+
import { useSearchParams } from 'react-router-dom';
510
/**
611
* Displays plugins for users to search, browse and rate.
712
* // todo: dynamically load plugins as user scrolls instead of fetching wholesale from backend
813
*/
914
const Plugins: React.FC = () => {
15+
const [searchParams] = useSearchParams()
16+
const searchQuery = searchParams.get('searchQuery') || "";
17+
18+
const { plugins, isLoading, error } = useFetchPlugins(Endpoints.fetchApiPlugins, 30, 1,searchQuery );
19+
1020
return (
11-
<div className="flex items-center justify-center h-screen bg-black">
12-
<div className="bg-white p-10 rounded-lg shadow-lg text-center max-w-screen-md">
13-
<h1 className="text-4xl font-bold mb-4 text-gray-800">Coming Soon</h1>
14-
<p className="text-lg text-gray-600 mb-8">
15-
Plugins are not ready. Please check back later!
16-
</p>
17-
<p className="text-md text-gray-500 mb-4">
18-
In the meantime, feel free to browse our available themes.
19-
</p>
20-
<Link
21-
to="/themes"
22-
className="inline-block bg-blue-500 text-white py-2 px-4 rounded-lg
23-
hover:bg-blue-600 transition duration-300"
24-
>
25-
Browse Themes
26-
</Link>
21+
<div className=" min-h-screen bg-black">
22+
<div className='p-8 flex flex-col gap-8 max-w-[1024px] m-auto'>
23+
<PluginSearchBar />
24+
<div className='grid xs:grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-16 gap-y-8'>
25+
{isLoading ? Array.from({length : 9}).map((_ , i)=><Skeleton key={i} width="280px" containerClassName='m-auto' height="300px" />) :
26+
plugins.length > 0 && plugins.map(plugin =>{
27+
return <PluginCard key={plugin.id} plugin={plugin} />
28+
})}
29+
</div>
30+
{!isLoading && error && <h1 className='text-white text-lg flex items-center justify-center '>{error}</h1>
31+
}
32+
{
33+
!error && !isLoading && plugins.length == 0 &&
34+
<h1 className='text-white text-lg flex items-center justify-center '>No search results found...</h1>
35+
}
2736
</div>
2837
</div>
2938
);

tailwind.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ export default {
5555
'black': '#000000',
5656
'white': '#ffffff',
5757
'slate-400': '#94a3b8',
58-
'slate-500': '#64748b'
58+
'slate-500': '#64748b',
59+
'card-border': '#27272A'
5960
},
6061
keyframes: {
6162
wiggle: {

0 commit comments

Comments
 (0)