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

View File

@@ -9,9 +9,9 @@ export const SnippetGrid = (props: Props): JSX.Element => {
const { snippets } = props;
return (
<div className='row'>
<div className='row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4'>
{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} />
</div>
))}

View File

@@ -1,14 +1,18 @@
interface Props {
title?: string;
children?: JSX.Element | JSX.Element[];
classes?: string;
bodyClasses?: string;
}
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 (
<div className='card mb-3'>
<div className='card-body'>
<div className={parentClasses}>
<div className={childClasses}>
<h5 className='card-title'>{title}</h5>
{children}
</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 (
<div className='col-12'>
<h2>{title}</h2>
<h4>{title}</h4>
{prevDest && (
<h6>
<Link

View File

@@ -4,3 +4,4 @@ export * from './Card';
export * from './PageHeader';
export * from './Spinner';
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 { SnippetsContext } from '../store';
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';
export const Snippets = (): JSX.Element => {
@@ -33,46 +40,46 @@ export const Snippets = (): JSX.Element => {
return (
<Layout>
<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;
{snippets.length === 0 ? (
<EmptyState />
) : (
<Fragment>
<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 (
<div
className={`d-flex justify-content-between cursor-pointer ${
isActiveFilter && 'text-dark 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='dark'
small
outline
handler={clearFilterHandler}
/>
return (
<div
className={`d-flex justify-content-between cursor-pointer ${
isActiveFilter && 'text-dark 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='dark'
small
outline
handler={clearFilterHandler}
/>
</div>
</Card>
</div>
</Card>
</div>
<div className='col-12 col-md-8 col-lg-9'>
{snippets.length > 0 ? (
<SnippetGrid snippets={localSnippets} />
) : (
<div className='col-12'>
<Spinner />
<div className='col-12 col-md-8 col-lg-9'>
<SnippetGrid snippets={localSnippets} />
</div>
)}
</div>
</Fragment>
)}
</Layout>
);
};

View File

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

View File

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