mirror of
https://github.com/pawelmalak/snippet-box.git
synced 2025-12-21 21:33:10 +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;
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
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 './Badge';
|
||||
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
|
||||
FROM snippets
|
||||
GROUP BY language
|
||||
ORDER BY
|
||||
count DESC,
|
||||
language ASC`,
|
||||
ORDER BY language ASC`,
|
||||
{
|
||||
type: QueryTypes.SELECT
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user