Skip to content

Commit

Permalink
repository - functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
guramidev committed Jul 26, 2021
1 parent b0e6d64 commit 5c51ab4
Show file tree
Hide file tree
Showing 20 changed files with 477 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/App.tsx
Expand Up @@ -7,6 +7,7 @@ import { QueryParamProvider } from 'use-query-params';
import './index.css';
import Auth from './features/auth/Auth';
import UserMiddleware from './features/auth/components/UserMiddleware/UserMiddleware';
import Dashboard from './features/dashboard/Dashboard';
import FullscreenProgress from './shared/components/FullscreenProgress/FullscreenProgress';
import { persistor, store } from './shared/redux/store';

Expand All @@ -19,6 +20,7 @@ const App = () => {
<CssBaseline />
<UserMiddleware>
<Auth />
<Dashboard />
</UserMiddleware>
</QueryParamProvider>
</Router>
Expand Down
19 changes: 19 additions & 0 deletions src/api/github/repository/api.ts
@@ -0,0 +1,19 @@
import { endpoint } from '@octokit/endpoint';
import { createApi } from '@reduxjs/toolkit/query/react';
import { githubBaseQuery } from '../index';
import { ResponseWithLink } from '../types';
import { RepositorySearchArgs, RepositorySearchData } from './types';

export const REPOSITORY_API_REDUCER_KEY = 'repositoryApi';
export const repositoryApi = createApi({
reducerPath: REPOSITORY_API_REDUCER_KEY,
baseQuery: githubBaseQuery,
endpoints: (builder) => ({
searchRepositories: builder.query<ResponseWithLink<RepositorySearchData>, RepositorySearchArgs>({
query: (args) => {
return endpoint('GET /search/repositories', args);
},
}),
}),
refetchOnMountOrArgChange: 60
});
12 changes: 12 additions & 0 deletions src/api/github/repository/enums.ts
@@ -0,0 +1,12 @@
export enum RepositoryVisibilityEnum {
PUBLIC,
PRIVATE
}

export enum SearchRepositorySortEnum {
STARS = 'stars',
FORKS = 'forks',
HELP_WANTED_ISSUES = 'help-wanted-issues',
UPDATED = 'updated',
BEST_MATCH = 'best-match'
}
7 changes: 7 additions & 0 deletions src/api/github/repository/types.ts
@@ -0,0 +1,7 @@
import { components, operations } from '@octokit/openapi-types/dist-types/generated/types';
import { Endpoints } from '@octokit/types';

export type Repository = components['schemas']['repo-search-result-item'];

export type RepositorySearchData = Endpoints['GET /search/repositories']['response']['data'];
export type RepositorySearchArgs = operations['search/repos']['parameters']['query'];
17 changes: 17 additions & 0 deletions src/features/dashboard/Dashboard.tsx
@@ -0,0 +1,17 @@
import React from 'react';
import { Redirect, Switch } from 'react-router-dom';
import AuthenticatedRoute from '../auth/components/AuthenticatedRoute/AuthenticatedRoute';
import Repositories from './features/repositories/Repositories';

const Dashboard = () => {
return (
<Switch>
<AuthenticatedRoute exact path="/">
<Redirect to="/repositories" />
</AuthenticatedRoute>
<Repositories />
</Switch>
);
};

export default Dashboard;
19 changes: 19 additions & 0 deletions src/features/dashboard/features/repositories/Repositories.tsx
@@ -0,0 +1,19 @@
import React, { Suspense } from 'react';
import { Switch } from 'react-router-dom';
import FullscreenProgress from '../../../../shared/components/FullscreenProgress/FullscreenProgress';
import AuthenticatedRoute from '../../../auth/components/AuthenticatedRoute/AuthenticatedRoute';

const RepositoriesRoute = React.lazy(() => import('./routes/Repositories/Repositories'));
const Repositories = () => {
return (
<Suspense fallback={<FullscreenProgress />}>
<Switch>
<AuthenticatedRoute exact path="/repositories">
<RepositoriesRoute />
</AuthenticatedRoute>
</Switch>
</Suspense>
);
};

export default Repositories;
@@ -0,0 +1,31 @@
import { Grid } from '@material-ui/core';
import React from 'react';
import PageContainer from '../../../../../../shared/components/PageContainer/PageContainer';
import PageHeader from '../../../../../../shared/components/PageHeader/PageHeader';
import RepositoryGrid from './components/RepositoryGrid/RepositoryGrid';
import RepositoryPagination from './components/RepositoryPagination/RepositoryPagination';
import RepositorySearch from './components/RepositorySearch/RepositorySearch';
import RepositorySearchFormContext from './components/RepositorySearch/RepositorySearchFormContext';

const Repositories = () => {
return (
<RepositorySearchFormContext>
<PageContainer>
<PageHeader title="Repositories" />
<Grid container spacing={3}>
<Grid item xs={12}>
<RepositorySearch />
</Grid>
<Grid item xs={12}>
<RepositoryGrid />
</Grid>
<Grid item xs={12}>
<RepositoryPagination />
</Grid>
</Grid>
</PageContainer>
</RepositorySearchFormContext>
)
}

export default Repositories;
@@ -0,0 +1,26 @@
import { Grid } from '@material-ui/core';
import React, { useMemo } from 'react';
import GridProgress from '../../../../../../../../shared/components/GridProgress/GridProgress';
import { useSearchRepositories } from '../../hooks/useSearchRepositories';
import RepositoryGridItem from '../RepositoryGridItem/RepositoryGridItem';

const RepositoryGrid = () => {
const { data, isFetching, isUninitialized } = useSearchRepositories();
const isLoading = isFetching || isUninitialized;

return useMemo(() => (
<GridProgress
container
spacing={2}
loading={isLoading}
>
{data?.response.items?.map((repo: any) => (
<Grid item sm={12} key={repo.id}>
<RepositoryGridItem repo={repo} />
</Grid>
))}
</GridProgress>
), [isLoading, data]);
}

export default RepositoryGrid;
@@ -0,0 +1,59 @@
import { Badge, Box, Chip, Divider, Grid, Typography } from '@material-ui/core';
import StarOutlineIcon from '@material-ui/icons/StarOutline';
import formatDistance from 'date-fns/formatDistance';
import React, { FC } from 'react';
import { Repository } from '../../../../../../../../api/github/repository/types';

const RepositoryGridItem: FC<{ repo: Repository }> = ({
repo
}) => {
return (
<Grid container spacing={1}>
<Grid item xs={12}>
<Typography variant="subtitle1" gutterBottom>
{repo.name}
<Box marginLeft={1} clone>
<Chip label={repo.private ? 'Private' : 'Public'} size="small" />
</Box>
</Typography>
<Typography component="p" variant="subtitle2" gutterBottom color="textSecondary">
{repo.description}
</Typography>
</Grid>
<Grid item xs={12}>
<Grid container alignItems="center" spacing={2}>
<Box clone flex="0 0 auto" display="flex" alignItems="center" marginRight={2}>
<Grid item>
<Box clone marginRight={1} marginLeft={0.5}>
<Badge color="primary" variant="dot" />
</Box>
<Typography variant="body2" color="textSecondary">
{repo.language}
</Typography>
</Grid>
</Box>
<Box clone flex="0 0 auto" display="flex" alignItems="center" marginRight={2}>
<Grid item>
<Box clone marginRight={0.5}>
<StarOutlineIcon fontSize="small" />
</Box>
<Typography variant="body2" color="textSecondary">
{repo.stargazers_count}
</Typography>
</Grid>
</Box>
<Grid item>
<Typography variant="body2" color="textSecondary">
Updated {formatDistance(new Date(repo.pushed_at), new Date())} ago
</Typography>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
</Grid>
)
}

export default RepositoryGridItem;
@@ -0,0 +1,38 @@
import TablePagination, { TablePaginationProps } from '@material-ui/core/TablePagination';
import React, { FC, useCallback, useMemo } from 'react';
import { DEFAULT_ROWS_PER_PAGE } from '../../../../../../../../shared/constants/table';
import { useSearchRepositoriesState } from '../../hooks/useSearchRepositories';
import { useRepositorySearchFormContext } from '../../hooks/useRepositorySearchFormContext';
import { repositorySearchFormDefaultValues } from '../RepositorySearch/RepositorySearchFormContext';

export const repositoriesPerPage = [
...DEFAULT_ROWS_PER_PAGE
]

const RepositoryPagination: FC = () => {
const { values, setFieldValue } = useRepositorySearchFormContext();
const { data } = useSearchRepositoriesState();

const handlePageChange: TablePaginationProps['onChangePage'] = useCallback((e, page) => {
setFieldValue('page', page + 1);
}, [setFieldValue]);

const handlePerPageChange: TablePaginationProps['onChangeRowsPerPage'] = useCallback((e) => {
setFieldValue('per_page', Number(e.target.value));
setFieldValue('page', repositorySearchFormDefaultValues.page);
}, [setFieldValue]);

return useMemo(() => (
<TablePagination
component="div"
count={data?.response?.total_count || 0}
page={values.page - 1}
onChangePage={handlePageChange}
rowsPerPage={values.per_page}
onChangeRowsPerPage={handlePerPageChange}
rowsPerPageOptions={repositoriesPerPage}
/>
), [data, values, handlePageChange, handlePerPageChange]);
}

export default RepositoryPagination;
@@ -0,0 +1,83 @@
import {
FormControl,
Grid,
InputLabel,
MenuItem,
Select,
TextField
} from '@material-ui/core';
import { useFormikContext } from 'formik';
import React, { FC, useCallback, useMemo } from 'react';
import {
RepositoryVisibilityEnum,
SearchRepositorySortEnum
} from '../../../../../../../../api/github/repository/enums';
import { repositorySearchFormDefaultValues } from './RepositorySearchFormContext';
import { RepositorySearchFormValues } from './types';

const RepositorySearch: FC = () => {
const { values, handleChange, touched, setFieldValue } = useFormikContext<RepositorySearchFormValues>();

const handleChangeAndResetPage = useCallback((e: React.ChangeEvent<any>) => {
handleChange(e);
setFieldValue('page', repositorySearchFormDefaultValues.page);
}, [handleChange, setFieldValue]);

return useMemo(() => (
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
fullWidth
name="name"
value={values.name}
onChange={handleChangeAndResetPage}
error={touched.name}
helperText={touched.name}
placeholder="Find a repository..."
variant="outlined"
size="small"
/>
</Grid>
<Grid item xs={3}>
<FormControl fullWidth variant="outlined" size="small">
<InputLabel>Type</InputLabel>
<Select
name="type"
value={values.type}
onChange={handleChangeAndResetPage}
error={touched.type}
inputProps={{
'aria-label': 'type'
}}
>
<MenuItem value="">All</MenuItem>
<MenuItem value={RepositoryVisibilityEnum.PUBLIC}>Public</MenuItem>
<MenuItem value={RepositoryVisibilityEnum.PRIVATE}>Private</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={3}>
<FormControl fullWidth variant="outlined" size="small">
<InputLabel>Sort</InputLabel>
<Select
name="sort"
value={values.sort}
onChange={handleChange}
error={touched.sort}
inputProps={{
'aria-label': 'sort'
}}
>
<MenuItem value={SearchRepositorySortEnum.BEST_MATCH}>{SearchRepositorySortEnum.BEST_MATCH}</MenuItem>
<MenuItem value={SearchRepositorySortEnum.FORKS}>{SearchRepositorySortEnum.FORKS}</MenuItem>
<MenuItem value={SearchRepositorySortEnum.HELP_WANTED_ISSUES}>{SearchRepositorySortEnum.HELP_WANTED_ISSUES}</MenuItem>
<MenuItem value={SearchRepositorySortEnum.STARS}>{SearchRepositorySortEnum.STARS}</MenuItem>
<MenuItem value={SearchRepositorySortEnum.UPDATED}>{SearchRepositorySortEnum.UPDATED}</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>
), [values, handleChange, touched, handleChangeAndResetPage]);
}

export default RepositorySearch;
@@ -0,0 +1,25 @@
import { Formik } from 'formik';
import { noop } from 'lodash';
import React, { FC } from 'react';
import { SearchRepositorySortEnum } from '../../../../../../../../api/github/repository/enums';

export const repositorySearchFormDefaultValues = {
name: '',
type: '',
sort: SearchRepositorySortEnum.BEST_MATCH,
per_page: 5,
page: 1
}

const RepositorySearchFormContext: FC = ({ children }) => {
return (
<Formik
initialValues={repositorySearchFormDefaultValues}
onSubmit={noop}
>
{children}
</Formik>
)
}

export default RepositorySearchFormContext;
@@ -0,0 +1,7 @@
import { RepositoryVisibilityEnum } from '../../../../../../../../api/github/repository/enums';
import { RepositorySearchArgs } from '../../../../../../../../api/github/repository/types';

export type RepositorySearchFormValues = {
name: string;
type: RepositoryVisibilityEnum;
} & Required<Pick<RepositorySearchArgs, 'sort' | 'per_page' | 'page'>>
@@ -0,0 +1,6 @@
import { useFormikContext } from 'formik';
import { RepositorySearchFormValues } from '../components/RepositorySearch/types';

export const useRepositorySearchFormContext = () => {
return useFormikContext<RepositorySearchFormValues>();
}

0 comments on commit 5c51ab4

Please sign in to comment.