mirror of
https://github.com/pawelmalak/snippet-box.git
synced 2025-12-24 06:28:07 +01:00
Added Snippets page with all snippets and filters
This commit is contained in:
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.css
|
||||||
2
client/src/bootstrap.min.css
vendored
2
client/src/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
24
client/src/components/Snippets/SnippetCard.tsx
Normal file
24
client/src/components/Snippets/SnippetCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
20
client/src/components/Snippets/SnippetGrid.tsx
Normal file
20
client/src/components/Snippets/SnippetGrid.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,15 +4,21 @@ interface Props {
|
|||||||
text: string;
|
text: string;
|
||||||
color: Color;
|
color: Color;
|
||||||
outline?: boolean;
|
outline?: boolean;
|
||||||
|
small?: boolean;
|
||||||
|
handler?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button = (props: Props): JSX.Element => {
|
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 (
|
return (
|
||||||
<button type='button' className={classes.join(' ')}>
|
<button type='button' className={classes.join(' ')} onClick={handler}>
|
||||||
{text}
|
{text}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const Card = (props: Props): JSX.Element => {
|
|||||||
const { title, children } = props;
|
const { title, children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='card'>
|
<div className='card mb-3'>
|
||||||
<div className='card-body'>
|
<div className='card-body'>
|
||||||
<h5 className='card-title'>{title}</h5>
|
<h5 className='card-title'>{title}</h5>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ interface Props {
|
|||||||
|
|
||||||
export const Layout = (props: Props): JSX.Element => {
|
export const Layout = (props: Props): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className='container'>
|
<div className='container-fluid px-5'>
|
||||||
<div className='row pt-4'>{props.children}</div>
|
<div className='row pt-4'>{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
59
client/src/components/UI/Spinner.module.css
Normal file
59
client/src/components/UI/Spinner.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
client/src/components/UI/Spinner.tsx
Normal file
5
client/src/components/UI/Spinner.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import classes from './Spinner.module.css';
|
||||||
|
|
||||||
|
export const Spinner = (): JSX.Element => {
|
||||||
|
return <div className={classes.Spinner}>Loading...</div>;
|
||||||
|
};
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
export * from './Layout';
|
export * from './Layout';
|
||||||
export * from './Badge';
|
export * from './Badge';
|
||||||
export * from './Card';
|
export * from './Card';
|
||||||
|
export * from './PageHeader';
|
||||||
|
export * from './Spinner';
|
||||||
|
export * from './Button';
|
||||||
|
export * from './List';
|
||||||
|
|||||||
74
client/src/containers/Snippets.tsx
Normal file
74
client/src/containers/Snippets.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -130,9 +130,7 @@ export const countSnippets = asyncWrapper(
|
|||||||
language
|
language
|
||||||
FROM snippets
|
FROM snippets
|
||||||
GROUP BY language
|
GROUP BY language
|
||||||
ORDER BY
|
ORDER BY language ASC`,
|
||||||
count DESC,
|
|
||||||
language ASC`,
|
|
||||||
{
|
{
|
||||||
type: QueryTypes.SELECT
|
type: QueryTypes.SELECT
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user