Pinned snippets

This commit is contained in:
unknown
2021-09-22 14:58:24 +02:00
parent 0fe96d3465
commit 956c281a98
15 changed files with 149 additions and 21 deletions

View File

@@ -1844,6 +1844,16 @@
}
}
},
"@mdi/js": {
"version": "6.1.95",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.1.95.tgz",
"integrity": "sha512-e6ZXoNB9uciA4smHHVkZWyYX/RRZsza8XfLvnOuvdLQttpzRKTqR26jG/COL0o4ES9vbAk9PX5mXTEstg0TCsg=="
},
"@mdi/react": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.5.0.tgz",
"integrity": "sha512-NztRgUxSYD+ImaKN94Tg66VVVqXj4SmlDGzZoz48H9riJ+Awha56sfXH2fegw819NWo7KI3oeS1Es0lNQqwr0w=="
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",

View File

@@ -4,6 +4,8 @@
"private": true,
"dependencies": {
"@icons-pack/react-simple-icons": "^4.6.1",
"@mdi/js": "^6.1.95",
"@mdi/react": "^1.5.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",

View File

@@ -1,16 +1,19 @@
import { Link } from 'react-router-dom';
import { useContext } from 'react';
import { Snippet } from '../../typescript/interfaces';
import { dateParser } from '../../utils';
import { dateParser, badgeColor } from '../../utils';
import { Badge, Button, Card } from '../UI';
import { SnippetsContext } from '../../store';
import Icon from '@mdi/react';
import { mdiPin } from '@mdi/js';
interface Props {
snippet: Snippet;
}
export const SnippetCard = (props: Props): JSX.Element => {
const { title, description, language, code, id, updatedAt } = props.snippet;
const { title, description, language, code, id, updatedAt, isPinned } =
props.snippet;
const { setSnippet } = useContext(SnippetsContext);
const copyHandler = () => {
@@ -18,15 +21,28 @@ export const SnippetCard = (props: Props): JSX.Element => {
};
return (
<Card title={title}>
<Card>
{/* TITLE */}
<h5 className='card-title d-flex align-items-center justify-content-between'>
{title}
{isPinned ? <Icon path={mdiPin} size={0.8} color='#212529' /> : ''}
</h5>
{/* UPDATE DATE */}
<h6 className='card-subtitle mb-2 text-muted'>
{dateParser(updatedAt).relative}
</h6>
{/* DESCRIPTION */}
<p className='text-truncate'>
{description ? description : 'No description'}
</p>
<Badge text={language} color='success' />
{/* LANGUAGE */}
<Badge text={language} color={badgeColor(language)} />
<hr />
{/* ACTIONS */}
<div className='d-flex justify-content-end'>
<Link
to={{

View File

@@ -4,16 +4,27 @@ import { SnippetsContext } from '../../store';
import { Snippet } from '../../typescript/interfaces';
import { dateParser } from '../../utils';
import { Button, Card } from '../UI';
import Icon from '@mdi/react';
import { mdiPin } from '@mdi/js';
interface Props {
snippet: Snippet;
}
export const SnippetDetails = (props: Props): JSX.Element => {
const { title, language, createdAt, updatedAt, description, code, id } =
props.snippet;
const {
title,
language,
createdAt,
updatedAt,
description,
code,
id,
isPinned
} = props.snippet;
const { deleteSnippet } = useContext(SnippetsContext);
const { deleteSnippet, toggleSnippetPin, setSnippet } =
useContext(SnippetsContext);
const creationDate = dateParser(createdAt);
const updateDate = dateParser(updatedAt);
@@ -23,7 +34,11 @@ export const SnippetDetails = (props: Props): JSX.Element => {
};
return (
<Card title={title}>
<Card>
<h5 className='card-title d-flex align-items-center justify-content-between'>
{title}
{isPinned ? <Icon path={mdiPin} size={0.8} color='#212529' /> : ''}
</h5>
<p>{description}</p>
{/* LANGUAGE */}
@@ -54,14 +69,21 @@ export const SnippetDetails = (props: Props): JSX.Element => {
state: { from: window.location.pathname }
}}
>
<Button text='Edit' color='dark' small outline classes='me-3' />
<Button
text='Edit'
color='dark'
small
outline
classes='me-3'
handler={() => setSnippet(id)}
/>
</Link>
<Button
text='Pin snippet'
text={`${isPinned ? 'Unpin snippet' : 'Pin snippet'}`}
color='dark'
small
outline
handler={copyHandler}
handler={() => toggleSnippetPin(id)}
classes='me-3'
/>
<Button

View File

@@ -24,7 +24,8 @@ export const SnippetForm = (props: Props): JSX.Element => {
description: '',
language: '',
code: '',
docs: ''
docs: '',
isPinned: false
});
useEffect(() => {

View File

@@ -1,19 +1,26 @@
import { Link } from 'react-router-dom';
interface Props {
interface Props<T> {
title: string;
prevDest?: string;
prevState?: T;
}
export const PageHeader = (props: Props): JSX.Element => {
const { title, prevDest } = props;
export const PageHeader = <T,>(props: Props<T>): JSX.Element => {
const { title, prevDest, prevState } = props;
return (
<div className='col-12'>
<h2>{title}</h2>
{prevDest && (
<h6>
<Link to={prevDest} className='text-decoration-none text-dark'>
<Link
to={{
pathname: prevDest,
state: prevState
}}
className='text-decoration-none text-dark'
>
&lt;- Go back
</Link>
</h6>

View File

@@ -21,19 +21,21 @@ export const Editor = (): JSX.Element => {
// Set snippet
useEffect(() => {
setCurrentSnippet(-1);
if (id) {
setCurrentSnippet(+id);
setInEdit(true);
}
}, [id, setCurrentSnippet]);
}, []);
return (
<Layout>
{inEdit ? (
<Fragment>
<PageHeader title='Edit snippet' prevDest={from} />
<PageHeader<{ from: string }>
title='Edit snippet'
prevDest={from}
prevState={{ from: '/snippets' }}
/>
<SnippetForm inEdit />
</Fragment>
) : (

View File

@@ -19,6 +19,7 @@ export const SnippetsContext = createContext<Context>({
createSnippet: (snippet: NewSnippet) => {},
updateSnippet: (snippet: NewSnippet, id: number) => {},
deleteSnippet: (id: number) => {},
toggleSnippetPin: (id: number) => {},
countSnippets: () => {}
});
@@ -48,6 +49,8 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
};
const setSnippet = (id: number): void => {
getSnippetById(id);
if (id < 0) {
setCurrentSnippet(null);
return;
@@ -82,7 +85,10 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
...snippets.slice(oldSnippetIdx + 1)
]);
setCurrentSnippet(res.data.data);
history.push(`/snippet/${res.data.data.id}`, { from: '/snippets' });
history.push({
pathname: `/snippet/${res.data.data.id}`,
state: { from: '/snippets' }
});
})
.catch(err => console.log(err));
};
@@ -104,6 +110,14 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
}
};
const toggleSnippetPin = (id: number): void => {
const snippet = snippets.find(s => s.id === id);
if (snippet) {
updateSnippet({ ...snippet, isPinned: !snippet.isPinned }, id);
}
};
const countSnippets = (): void => {
axios
.get<Response<LanguageCount[]>>('/api/snippets/statistics/count')
@@ -121,6 +135,7 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
createSnippet,
updateSnippet,
deleteSnippet,
toggleSnippetPin,
countSnippets
};

View File

@@ -10,5 +10,6 @@ export interface Context {
createSnippet: (snippet: NewSnippet) => void;
updateSnippet: (snippet: NewSnippet, id: number) => void;
deleteSnippet: (id: number) => void;
toggleSnippetPin: (id: number) => void;
countSnippets: () => void;
}

View File

@@ -6,6 +6,7 @@ export interface NewSnippet {
language: string;
code: string;
docs?: string;
isPinned: boolean;
}
export interface Snippet extends Model, NewSnippet {}

View File

@@ -0,0 +1,30 @@
import { Color } from '../typescript/types';
export const badgeColor = (language: string): Color => {
const code = language.toLowerCase().charCodeAt(0);
let color: Color = 'primary';
switch (code % 6) {
case 0:
default:
color = 'primary';
break;
case 1:
color = 'success';
break;
case 2:
color = 'info';
break;
case 3:
color = 'warning';
break;
case 4:
color = 'danger';
break;
case 5:
color = 'dark';
break;
}
return color;
};

View File

@@ -1 +1,2 @@
export * from './dateParser';
export * from './badgeColor';