Home page. Empty state component

This commit is contained in:
unknown
2021-09-23 11:31:26 +02:00
parent 956c281a98
commit 02c607ba92
10 changed files with 191 additions and 126 deletions

View File

@@ -12,7 +12,7 @@ interface Props {
} }
export const SnippetCard = (props: Props): JSX.Element => { export const SnippetCard = (props: Props): JSX.Element => {
const { title, description, language, code, id, updatedAt, isPinned } = const { title, description, language, code, id, createdAt, isPinned } =
props.snippet; props.snippet;
const { setSnippet } = useContext(SnippetsContext); const { setSnippet } = useContext(SnippetsContext);
@@ -21,47 +21,47 @@ export const SnippetCard = (props: Props): JSX.Element => {
}; };
return ( return (
<Card> <Card classes='h-100' bodyClasses='d-flex flex-column'>
{/* TITLE */} {/* TITLE */}
<h5 className='card-title d-flex align-items-center justify-content-between'> <h5 className='card-title d-flex align-items-center justify-content-between'>
{title} {title}
{isPinned ? <Icon path={mdiPin} size={0.8} color='#212529' /> : ''} {isPinned ? <Icon path={mdiPin} size={0.8} color='#212529' /> : ''}
</h5> </h5>
{/* UPDATE DATE */}
<h6 className='card-subtitle mb-2 text-muted'> <h6 className='card-subtitle mb-2 text-muted'>
{dateParser(updatedAt).relative} {/* LANGUAGE */}
<Badge text={language} color={badgeColor(language)} />
</h6> </h6>
{/* DESCRIPTION */} {/* DESCRIPTION */}
<p className='text-truncate'> <p>{description ? description : 'No description'}</p>
{description ? description : 'No description'}
</p>
{/* LANGUAGE */} <div className='mt-auto'>
<Badge text={language} color={badgeColor(language)} /> {/* UPDATE DATE */}
<hr /> <p>Created {dateParser(createdAt).relative}</p>
<hr />
{/* ACTIONS */} {/* ACTIONS */}
<div className='d-flex justify-content-end'> <div className='d-flex justify-content-end'>
<Link <Link
to={{ to={{
pathname: `/snippet/${id}`, pathname: `/snippet/${id}`,
state: { from: window.location.pathname } state: { from: window.location.pathname }
}}
>
<Button
text='View'
color='dark'
small
outline
classes='me-2'
handler={() => {
setSnippet(id);
}} }}
/> >
</Link> <Button
<Button text='Copy code' color='dark' small handler={copyHandler} /> text='View'
color='dark'
small
outline
classes='me-2'
handler={() => {
setSnippet(id);
}}
/>
</Link>
<Button text='Copy code' color='dark' small handler={copyHandler} />
</div>
</div> </div>
</Card> </Card>
); );

View File

@@ -9,9 +9,9 @@ export const SnippetGrid = (props: Props): JSX.Element => {
const { snippets } = props; const { snippets } = props;
return ( return (
<div className='row'> <div className='row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4'>
{snippets.map(snippet => ( {snippets.map(snippet => (
<div className='col-12 col-md-6 col-lg-4' key={snippet.id}> <div className='col' key={snippet.id}>
<SnippetCard snippet={snippet} /> <SnippetCard snippet={snippet} />
</div> </div>
))} ))}

View File

@@ -1,14 +1,18 @@
interface Props { interface Props {
title?: string; title?: string;
children?: JSX.Element | JSX.Element[]; children?: JSX.Element | JSX.Element[];
classes?: string;
bodyClasses?: string;
} }
export const Card = (props: Props): JSX.Element => { export const Card = (props: Props): JSX.Element => {
const { title, children } = props; const { title, children, classes, bodyClasses } = props;
const parentClasses = `card mb-3 ${classes}`;
const childClasses = `card-body ${bodyClasses}`;
return ( return (
<div className='card mb-3'> <div className={parentClasses}>
<div className='card-body'> <div className={childClasses}>
<h5 className='card-title'>{title}</h5> <h5 className='card-title'>{title}</h5>
{children} {children}
</div> </div>

View File

@@ -0,0 +1,16 @@
import { Link } from 'react-router-dom';
export const EmptyState = (): JSX.Element => {
const editorLink = (
<Link to='/editor' className='fw-bold text-decoration-none text-dark'>
<span>editor</span>
</Link>
);
return (
<div className='col-12 d-flex flex-column align-items-center'>
<h4 className='text-muted'>You currently don't have any snippets</h4>
<p>Go to the {editorLink} and create one</p>
</div>
);
};

View File

@@ -11,7 +11,7 @@ export const PageHeader = <T,>(props: Props<T>): JSX.Element => {
return ( return (
<div className='col-12'> <div className='col-12'>
<h2>{title}</h2> <h4>{title}</h4>
{prevDest && ( {prevDest && (
<h6> <h6>
<Link <Link

View File

@@ -4,3 +4,4 @@ export * from './Card';
export * from './PageHeader'; export * from './PageHeader';
export * from './Spinner'; export * from './Spinner';
export * from './Button'; export * from './Button';
export * from './EmptyState';

View File

@@ -0,0 +1,38 @@
import { useEffect, useContext, Fragment } from 'react';
import { SnippetsContext } from '../store';
import { Layout, Spinner, PageHeader, EmptyState } from '../components/UI';
import { SnippetGrid } from '../components/Snippets/SnippetGrid';
export const Home = (): JSX.Element => {
const { snippets, getSnippets } = useContext(SnippetsContext);
useEffect(() => {
getSnippets();
}, []);
return (
<Layout>
{snippets.length === 0 ? (
<EmptyState />
) : (
<Fragment>
{snippets.some(s => s.isPinned) && (
<Fragment>
<PageHeader title='Pinned snippets' />
<div className='col-12 mb-3'>
<SnippetGrid snippets={snippets.filter(s => s.isPinned)} />
</div>
</Fragment>
)}
<PageHeader title='Recent snippets' />
<div className='col-12'>
<SnippetGrid
snippets={snippets.filter(s => !s.isPinned).slice(0, 6)}
/>
</div>
</Fragment>
)}
</Layout>
);
};

View File

@@ -1,7 +1,14 @@
import { useEffect, useContext, useState, Fragment } from 'react'; import { useEffect, useContext, useState, Fragment } from 'react';
import { SnippetsContext } from '../store'; import { SnippetsContext } from '../store';
import { SnippetGrid } from '../components/Snippets/SnippetGrid'; import { SnippetGrid } from '../components/Snippets/SnippetGrid';
import { Badge, Button, Card, Layout, Spinner } from '../components/UI'; import {
Badge,
Button,
Card,
EmptyState,
Layout,
Spinner
} from '../components/UI';
import { Snippet } from '../typescript/interfaces'; import { Snippet } from '../typescript/interfaces';
export const Snippets = (): JSX.Element => { export const Snippets = (): JSX.Element => {
@@ -33,46 +40,46 @@ export const Snippets = (): JSX.Element => {
return ( return (
<Layout> <Layout>
<div className='col-12 col-md-4 col-lg-3'> {snippets.length === 0 ? (
<Card title='Filter by language'> <EmptyState />
<Fragment> ) : (
{languageCount.map((el, idx) => { <Fragment>
const isActiveFilter = filter === el.language; <div className='col-12 col-md-4 col-lg-3'>
<Card title='Filter by language'>
<Fragment>
{languageCount.map((el, idx) => {
const isActiveFilter = filter === el.language;
return ( return (
<div <div
className={`d-flex justify-content-between cursor-pointer ${ className={`d-flex justify-content-between cursor-pointer ${
isActiveFilter && 'text-dark fw-bold' isActiveFilter && 'text-dark fw-bold'
}`} }`}
key={idx} key={idx}
onClick={() => filterHandler(el.language)} onClick={() => filterHandler(el.language)}
> >
<span>{el.language}</span> <span>{el.language}</span>
<span>{el.count}</span> <span>{el.count}</span>
</div> </div>
); );
})} })}
</Fragment> </Fragment>
<div className='d-grid mt-3'> <div className='d-grid mt-3'>
<Button <Button
text='Clear filters' text='Clear filters'
color='dark' color='dark'
small small
outline outline
handler={clearFilterHandler} handler={clearFilterHandler}
/> />
</div>
</Card>
</div> </div>
</Card> <div className='col-12 col-md-8 col-lg-9'>
</div> <SnippetGrid snippets={localSnippets} />
<div className='col-12 col-md-8 col-lg-9'>
{snippets.length > 0 ? (
<SnippetGrid snippets={localSnippets} />
) : (
<div className='col-12'>
<Spinner />
</div> </div>
)} </Fragment>
</div> )}
</Layout> </Layout>
); );
}; };

View File

@@ -34,18 +34,22 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
const history = useHistory(); const history = useHistory();
const redirectOnError = () => {
history.push('/');
};
const getSnippets = (): void => { const getSnippets = (): void => {
axios axios
.get<Response<Snippet[]>>('/api/snippets') .get<Response<Snippet[]>>('/api/snippets')
.then(res => setSnippets(res.data.data)) .then(res => setSnippets(res.data.data))
.catch(err => console.log(err)); .catch(err => redirectOnError());
}; };
const getSnippetById = (id: number): void => { const getSnippetById = (id: number): void => {
axios axios
.get<Response<Snippet>>(`/api/snippets/${id}`) .get<Response<Snippet>>(`/api/snippets/${id}`)
.then(res => setCurrentSnippet(res.data.data)) .then(res => setCurrentSnippet(res.data.data))
.catch(err => console.log(err)); .catch(err => redirectOnError());
}; };
const setSnippet = (id: number): void => { const setSnippet = (id: number): void => {
@@ -69,9 +73,12 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
.then(res => { .then(res => {
setSnippets([...snippets, res.data.data]); setSnippets([...snippets, res.data.data]);
setCurrentSnippet(res.data.data); setCurrentSnippet(res.data.data);
history.push(`/snippet/${res.data.data.id}`); history.push({
pathname: `/snippet/${res.data.data.id}`,
state: { from: '/snippets' }
});
}) })
.catch(err => console.log(err)); .catch(err => redirectOnError());
}; };
const updateSnippet = (snippet: NewSnippet, id: number): void => { const updateSnippet = (snippet: NewSnippet, id: number): void => {
@@ -90,7 +97,7 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
state: { from: '/snippets' } state: { from: '/snippets' }
}); });
}) })
.catch(err => console.log(err)); .catch(err => redirectOnError());
}; };
const deleteSnippet = (id: number): void => { const deleteSnippet = (id: number): void => {
@@ -106,7 +113,7 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
setSnippet(-1); setSnippet(-1);
history.push('/snippets'); history.push('/snippets');
}) })
.catch(err => console.log(err)); .catch(err => redirectOnError());
} }
}; };
@@ -122,7 +129,7 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
axios axios
.get<Response<LanguageCount[]>>('/api/snippets/statistics/count') .get<Response<LanguageCount[]>>('/api/snippets/statistics/count')
.then(res => setLanguageCount(res.data.data)) .then(res => setLanguageCount(res.data.data))
.catch(err => console.log(err)); .catch(err => redirectOnError());
}; };
const context = { const context = {

View File

@@ -1,53 +1,45 @@
import { DataTypes, Model, QueryInterface } from 'sequelize'; import { DataTypes, QueryInterface } from 'sequelize';
import {
Snippet,
SnippetCreationAttributes
} from '../../typescript/interfaces';
const { INTEGER, STRING, DATE, TEXT } = DataTypes; const { INTEGER, STRING, DATE, TEXT } = DataTypes;
export const up = async (queryInterface: QueryInterface): Promise<void> => { export const up = async (queryInterface: QueryInterface): Promise<void> => {
await queryInterface.createTable<Model<Snippet, SnippetCreationAttributes>>( await queryInterface.createTable('snippets', {
'snippets', id: {
{ type: INTEGER,
id: { allowNull: false,
type: INTEGER, primaryKey: true,
allowNull: false, autoIncrement: true
primaryKey: true, },
autoIncrement: true title: {
}, type: STRING,
title: { allowNull: false
type: STRING, },
allowNull: false description: {
}, type: TEXT,
description: { allowNull: true,
type: TEXT, defaultValue: ''
allowNull: true, },
defaultValue: '' language: {
}, type: STRING,
language: { allowNull: false
type: STRING, },
allowNull: false code: {
}, type: TEXT,
code: { allowNull: false
type: TEXT, },
allowNull: false docs: {
}, type: TEXT,
docs: { allowNull: true,
type: TEXT, defaultValue: ''
allowNull: true, },
defaultValue: '' createdAt: {
}, type: DATE,
createdAt: { allowNull: false
type: DATE, },
allowNull: false updatedAt: {
}, type: DATE,
updatedAt: { allowNull: false
type: DATE,
allowNull: false
}
} }
); });
}; };
export const down = async (queryInterface: QueryInterface): Promise<void> => { export const down = async (queryInterface: QueryInterface): Promise<void> => {