mirror of
https://github.com/pawelmalak/snippet-box.git
synced 2025-12-21 21:33:10 +01:00
Pinned snippets
This commit is contained in:
10
client/package-lock.json
generated
10
client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,7 +24,8 @@ export const SnippetForm = (props: Props): JSX.Element => {
|
||||
description: '',
|
||||
language: '',
|
||||
code: '',
|
||||
docs: ''
|
||||
docs: '',
|
||||
isPinned: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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'
|
||||
>
|
||||
<- Go back
|
||||
</Link>
|
||||
</h6>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface NewSnippet {
|
||||
language: string;
|
||||
code: string;
|
||||
docs?: string;
|
||||
isPinned: boolean;
|
||||
}
|
||||
|
||||
export interface Snippet extends Model, NewSnippet {}
|
||||
|
||||
30
client/src/utils/badgeColor.ts
Normal file
30
client/src/utils/badgeColor.ts
Normal 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;
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export * from './dateParser';
|
||||
export * from './badgeColor';
|
||||
|
||||
Reference in New Issue
Block a user