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": { "@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",

View File

@@ -4,6 +4,8 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@icons-pack/react-simple-icons": "^4.6.1", "@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/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7", "@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3", "@testing-library/user-event": "^12.8.3",

View File

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

View File

@@ -4,16 +4,27 @@ import { SnippetsContext } from '../../store';
import { Snippet } from '../../typescript/interfaces'; import { Snippet } from '../../typescript/interfaces';
import { dateParser } from '../../utils'; import { dateParser } from '../../utils';
import { Button, Card } from '../UI'; import { Button, Card } from '../UI';
import Icon from '@mdi/react';
import { mdiPin } from '@mdi/js';
interface Props { interface Props {
snippet: Snippet; snippet: Snippet;
} }
export const SnippetDetails = (props: Props): JSX.Element => { export const SnippetDetails = (props: Props): JSX.Element => {
const { title, language, createdAt, updatedAt, description, code, id } = const {
props.snippet; 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 creationDate = dateParser(createdAt);
const updateDate = dateParser(updatedAt); const updateDate = dateParser(updatedAt);
@@ -23,7 +34,11 @@ export const SnippetDetails = (props: Props): JSX.Element => {
}; };
return ( 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> <p>{description}</p>
{/* LANGUAGE */} {/* LANGUAGE */}
@@ -54,14 +69,21 @@ export const SnippetDetails = (props: Props): JSX.Element => {
state: { from: window.location.pathname } 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> </Link>
<Button <Button
text='Pin snippet' text={`${isPinned ? 'Unpin snippet' : 'Pin snippet'}`}
color='dark' color='dark'
small small
outline outline
handler={copyHandler} handler={() => toggleSnippetPin(id)}
classes='me-3' classes='me-3'
/> />
<Button <Button

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ export const SnippetsContext = createContext<Context>({
createSnippet: (snippet: NewSnippet) => {}, createSnippet: (snippet: NewSnippet) => {},
updateSnippet: (snippet: NewSnippet, id: number) => {}, updateSnippet: (snippet: NewSnippet, id: number) => {},
deleteSnippet: (id: number) => {}, deleteSnippet: (id: number) => {},
toggleSnippetPin: (id: number) => {},
countSnippets: () => {} countSnippets: () => {}
}); });
@@ -48,6 +49,8 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
}; };
const setSnippet = (id: number): void => { const setSnippet = (id: number): void => {
getSnippetById(id);
if (id < 0) { if (id < 0) {
setCurrentSnippet(null); setCurrentSnippet(null);
return; return;
@@ -82,7 +85,10 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
...snippets.slice(oldSnippetIdx + 1) ...snippets.slice(oldSnippetIdx + 1)
]); ]);
setCurrentSnippet(res.data.data); 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)); .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 => { const countSnippets = (): void => {
axios axios
.get<Response<LanguageCount[]>>('/api/snippets/statistics/count') .get<Response<LanguageCount[]>>('/api/snippets/statistics/count')
@@ -121,6 +135,7 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
createSnippet, createSnippet,
updateSnippet, updateSnippet,
deleteSnippet, deleteSnippet,
toggleSnippetPin,
countSnippets countSnippets
}; };

View File

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

View File

@@ -6,6 +6,7 @@ export interface NewSnippet {
language: string; language: string;
code: string; code: string;
docs?: string; docs?: string;
isPinned: boolean;
} }
export interface Snippet extends Model, NewSnippet {} 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 './dateParser';
export * from './badgeColor';

View File

@@ -0,0 +1,14 @@
import { DataTypes, QueryInterface } from 'sequelize';
const { INTEGER } = DataTypes;
export const up = async (queryInterface: QueryInterface): Promise<void> => {
await queryInterface.addColumn('snippets', 'isPinned', {
type: INTEGER,
allowNull: true,
defaultValue: 0
});
};
export const down = async (queryInterface: QueryInterface): Promise<void> => {
await queryInterface.removeColumn('snippets', 'isPinned');
};

View File

@@ -36,6 +36,11 @@ export const SnippetModel = sequelize.define<SnippetInstance>('Snippet', {
allowNull: true, allowNull: true,
defaultValue: '' defaultValue: ''
}, },
isPinned: {
type: INTEGER,
allowNull: true,
defaultValue: 0
},
createdAt: { createdAt: {
type: DATE type: DATE
}, },

View File

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