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 => {
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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 (
|
return (
|
||||||
<div className='col-12'>
|
<div className='col-12'>
|
||||||
<h2>{title}</h2>
|
<h4>{title}</h4>
|
||||||
{prevDest && (
|
{prevDest && (
|
||||||
<h6>
|
<h6>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
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 { 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user