Added Snippets page with all snippets and filters

This commit is contained in:
unknown
2021-09-21 13:56:39 +02:00
parent 4feb069ac1
commit b8b0603b3f
13 changed files with 200 additions and 22 deletions

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
*.css

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,24 @@
import { Snippet } from '../../typescript/interfaces';
import { dateParser } from '../../utils';
import { Card } from '../UI';
interface Props {
snippet: Snippet;
}
export const SnippetCard = (props: Props): JSX.Element => {
const { title, description, language, code, id, updatedAt } = props.snippet;
const copyHandler = () => {
navigator.clipboard.writeText(code);
};
return (
<Card title={title}>
<h6 className='card-subtitle mb-2 text-muted'>
{dateParser(updatedAt).relative}
</h6>
<p onClick={copyHandler}>{language}</p>
</Card>
);
};

View File

@@ -0,0 +1,20 @@
import { Snippet } from '../../typescript/interfaces';
import { SnippetCard } from './SnippetCard';
interface Props {
snippets: Snippet[];
}
export const SnippetGrid = (props: Props): JSX.Element => {
const { snippets } = props;
return (
<div className='row'>
{snippets.map(snippet => (
<div className='col-12 col-md-6 col-lg-3' key={snippet.id}>
<SnippetCard snippet={snippet} />
</div>
))}
</div>
);
};

View File

@@ -4,15 +4,21 @@ interface Props {
text: string;
color: Color;
outline?: boolean;
small?: boolean;
handler?: () => void;
}
export const Button = (props: Props): JSX.Element => {
const { text, color, outline = false } = props;
const { text, color, outline = false, small = false, handler } = props;
const classes = ['btn', outline ? `btn-outline-${color}` : `btn-${color}`];
const classes = [
'btn',
outline ? `btn-outline-${color}` : `btn-${color}`,
small && 'btn-sm'
];
return (
<button type='button' className={classes.join(' ')}>
<button type='button' className={classes.join(' ')} onClick={handler}>
{text}
</button>
);

View File

@@ -7,7 +7,7 @@ export const Card = (props: Props): JSX.Element => {
const { title, children } = props;
return (
<div className='card'>
<div className='card mb-3'>
<div className='card-body'>
<h5 className='card-title'>{title}</h5>
{children}

View File

@@ -4,7 +4,7 @@ interface Props {
export const Layout = (props: Props): JSX.Element => {
return (
<div className='container'>
<div className='container-fluid px-5'>
<div className='row pt-4'>{props.children}</div>
</div>
);

View File

@@ -0,0 +1,59 @@
.Spinner,
.Spinner:before,
.Spinner:after {
background: #3459e6;
-webkit-animation: load1 1s infinite ease-in-out;
animation: load1 1s infinite ease-in-out;
width: 1em;
height: 4em;
}
.Spinner {
color: #3459e6;
text-indent: -9999em;
margin: 88px auto;
position: relative;
font-size: 11px;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
.Spinner:before,
.Spinner:after {
position: absolute;
top: 0;
content: '';
}
.Spinner:before {
left: -1.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.Spinner:after {
left: 1.5em;
}
@-webkit-keyframes load1 {
0%,
80%,
100% {
box-shadow: 0 0;
height: 4em;
}
40% {
box-shadow: 0 -2em;
height: 5em;
}
}
@keyframes load1 {
0%,
80%,
100% {
box-shadow: 0 0;
height: 4em;
}
40% {
box-shadow: 0 -2em;
height: 5em;
}
}

View File

@@ -0,0 +1,5 @@
import classes from './Spinner.module.css';
export const Spinner = (): JSX.Element => {
return <div className={classes.Spinner}>Loading...</div>;
};

View File

@@ -1,3 +1,7 @@
export * from './Layout';
export * from './Badge';
export * from './Card';
export * from './PageHeader';
export * from './Spinner';
export * from './Button';
export * from './List';

View File

@@ -0,0 +1,74 @@
import { useEffect, useContext, useState, Fragment } from 'react';
import { SnippetsContext } from '../store';
import { SnippetGrid } from '../components/Snippets/SnippetGrid';
import { Badge, Button, Card, Layout, List, Spinner } from '../components/UI';
import { Snippet } from '../typescript/interfaces';
export const Snippets = (): JSX.Element => {
const { snippets, languageCount, getSnippets, countSnippets } =
useContext(SnippetsContext);
const [filter, setFilter] = useState<string | null>(null);
const [localSnippets, setLocalSnippets] = useState<Snippet[]>([...snippets]);
useEffect(() => {
getSnippets();
countSnippets();
}, []);
const filterHandler = (language: string) => {
setFilter(language);
const filteredSnippets = snippets.filter(s => s.language === language);
setLocalSnippets(filteredSnippets);
};
const clearFilterHandler = () => {
setFilter(null);
setLocalSnippets([...snippets]);
};
return (
<Layout>
<div className='col-12 col-md-4 col-lg-2'>
<Card title='Filter by language'>
<Fragment>
{languageCount.map((el, idx) => {
const isActiveFilter = filter === el.language;
return (
<div
className={`d-flex justify-content-between cursor-pointer ${
isActiveFilter && 'text-primary fw-bold'
}`}
key={idx}
onClick={() => filterHandler(el.language)}
>
<span>{el.language}</span>
<span>{el.count}</span>
</div>
);
})}
</Fragment>
<div className='d-grid mt-3'>
<Button
text='Clear filters'
color='primary'
small
outline
handler={clearFilterHandler}
/>
</div>
</Card>
</div>
<div className='col-12 col-md-8 col-lg-10'>
{snippets.length > 0 ? (
<SnippetGrid snippets={localSnippets} />
) : (
<div className='col-12'>
<Spinner />
</div>
)}
</div>
</Layout>
);
};

View File

@@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -130,9 +130,7 @@ export const countSnippets = asyncWrapper(
language
FROM snippets
GROUP BY language
ORDER BY
count DESC,
language ASC`,
ORDER BY language ASC`,
{
type: QueryTypes.SELECT
}