mirror of
https://github.com/pawelmalak/snippet-box.git
synced 2025-12-21 13:23:05 +01:00
Home page. Empty state component
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
16
client/src/components/UI/EmptyState.tsx
Normal file
16
client/src/components/UI/EmptyState.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './Card';
|
||||
export * from './PageHeader';
|
||||
export * from './Spinner';
|
||||
export * from './Button';
|
||||
export * from './EmptyState';
|
||||
|
||||
38
client/src/containers/Home.tsx
Normal file
38
client/src/containers/Home.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
Reference in New Issue
Block a user