mirror of
https://github.com/pawelmalak/snippet-box.git
synced 2025-12-21 13:23:05 +01:00
React client with context for state management
This commit is contained in:
13
client/package-lock.json
generated
13
client/package-lock.json
generated
@@ -3178,6 +3178,14 @@
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.3.tgz",
|
||||
"integrity": "sha512-/lqqLAmuIPi79WYfRpy2i8z+x+vxU3zX2uAm0gs1q52qTuKwolOj1P8XbufpXcsydrpKx2yGn2wzAnxCMV86QA=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"axobject-query": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||
@@ -4907,6 +4915,11 @@
|
||||
"whatwg-url": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.10.7",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
|
||||
"integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"@types/node": "^12.20.25",
|
||||
"@types/react": "^17.0.21",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"axios": "^0.21.4",
|
||||
"dayjs": "^1.10.7",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^5.3.0",
|
||||
@@ -44,5 +46,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-router-dom": "^5.1.9"
|
||||
}
|
||||
},
|
||||
"proxy": "http://localhost:5000"
|
||||
}
|
||||
|
||||
BIN
client/public/favicon.ico
Normal file
BIN
client/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
43
client/public/index.html
Normal file
43
client/public/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
client/public/logo192.png
Normal file
BIN
client/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
client/public/logo512.png
Normal file
BIN
client/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
client/public/manifest.json
Normal file
25
client/public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
client/public/robots.txt
Normal file
3
client/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
16
client/src/App.tsx
Normal file
16
client/src/App.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { BrowserRouter, Switch, Route } from 'react-router-dom';
|
||||
import { Navbar } from './components/Navigation/Navbar';
|
||||
import { Home, Snippet, Snippets } from './containers';
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Navbar />
|
||||
<Switch>
|
||||
<Route exact path='/' component={Home} />
|
||||
<Route path='/snippets' component={Snippets} />
|
||||
<Route path='/snippet/:id' component={Snippet} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
12
client/src/bootstrap.min.css
vendored
Normal file
12
client/src/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
13
client/src/index.css
Normal file
13
client/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
14
client/src/index.tsx
Normal file
14
client/src/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './bootstrap.min.css';
|
||||
import { App } from './App';
|
||||
import { SnippetsContextProvider } from './store';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<SnippetsContextProvider>
|
||||
<App />
|
||||
</SnippetsContextProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
51
client/src/store/SnippetsContext.tsx
Normal file
51
client/src/store/SnippetsContext.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useState, createContext } from 'react';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
Context,
|
||||
Snippet,
|
||||
Response,
|
||||
LanguageCount
|
||||
} from '../typescript/interfaces';
|
||||
|
||||
export const SnippetsContext = createContext<Context>({
|
||||
snippets: [],
|
||||
languageCount: [],
|
||||
getSnippets: () => {},
|
||||
countSnippets: () => {}
|
||||
});
|
||||
|
||||
interface Props {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
export const SnippetsContextProvider = (props: Props): JSX.Element => {
|
||||
const [snippets, setSnippets] = useState<Snippet[]>([]);
|
||||
const [languageCount, setLanguageCount] = useState<LanguageCount[]>([]);
|
||||
|
||||
const getSnippets = (): void => {
|
||||
axios
|
||||
.get<Response<Snippet[]>>('/api/snippets')
|
||||
.then(res => setSnippets(res.data.data))
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const countSnippets = (): void => {
|
||||
axios
|
||||
.get<Response<LanguageCount[]>>('/api/snippets/statistics/count')
|
||||
.then(res => setLanguageCount(res.data.data))
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const context = {
|
||||
snippets,
|
||||
languageCount,
|
||||
getSnippets,
|
||||
countSnippets
|
||||
};
|
||||
|
||||
return (
|
||||
<SnippetsContext.Provider value={context}>
|
||||
{props.children}
|
||||
</SnippetsContext.Provider>
|
||||
);
|
||||
};
|
||||
1
client/src/store/index.ts
Normal file
1
client/src/store/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './SnippetsContext';
|
||||
8
client/src/typescript/interfaces/Context.ts
Normal file
8
client/src/typescript/interfaces/Context.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { LanguageCount, Snippet } from '.';
|
||||
|
||||
export interface Context {
|
||||
snippets: Snippet[];
|
||||
languageCount: LanguageCount[];
|
||||
getSnippets: () => void;
|
||||
countSnippets: () => void;
|
||||
}
|
||||
5
client/src/typescript/interfaces/Model.ts
Normal file
5
client/src/typescript/interfaces/Model.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Model {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
3
client/src/typescript/interfaces/Response.ts
Normal file
3
client/src/typescript/interfaces/Response.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface Response<T> {
|
||||
data: T;
|
||||
}
|
||||
4
client/src/typescript/interfaces/Route.ts
Normal file
4
client/src/typescript/interfaces/Route.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Route {
|
||||
name: string;
|
||||
dest: string;
|
||||
}
|
||||
11
client/src/typescript/interfaces/Snippet.ts
Normal file
11
client/src/typescript/interfaces/Snippet.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Model } from '.';
|
||||
|
||||
export interface NewSnippet {
|
||||
title: string;
|
||||
description?: string;
|
||||
language: string;
|
||||
code: string;
|
||||
docs?: string;
|
||||
}
|
||||
|
||||
export interface Snippet extends Model, NewSnippet {}
|
||||
4
client/src/typescript/interfaces/Statistics.ts
Normal file
4
client/src/typescript/interfaces/Statistics.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface LanguageCount {
|
||||
count: number;
|
||||
language: string;
|
||||
}
|
||||
6
client/src/typescript/interfaces/index.ts
Normal file
6
client/src/typescript/interfaces/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './Model';
|
||||
export * from './Snippet';
|
||||
export * from './Route';
|
||||
export * from './Response';
|
||||
export * from './Context';
|
||||
export * from './Statistics';
|
||||
17
client/src/utils/dateParser.ts
Normal file
17
client/src/utils/dateParser.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
interface Return {
|
||||
formatted: string;
|
||||
relative: string;
|
||||
}
|
||||
|
||||
export const dateParser = (date: Date): Return => {
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const parsedDate = dayjs(date);
|
||||
const formatted = parsedDate.format('YYYY-MM-DD HH:mm');
|
||||
const relative = parsedDate.fromNow();
|
||||
|
||||
return { formatted, relative };
|
||||
};
|
||||
1
client/src/utils/index.ts
Normal file
1
client/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dateParser';
|
||||
6
nodemon.json
Normal file
6
nodemon.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts,json",
|
||||
"ignore": ["src/**/*.spec.ts"],
|
||||
"exec": "ts-node ./src/server.ts"
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { QueryTypes } from 'sequelize';
|
||||
import { sequelize } from '../db';
|
||||
import { asyncWrapper } from '../middleware';
|
||||
import { SnippetModel } from '../models';
|
||||
import { ErrorResponse } from '../utils';
|
||||
@@ -114,3 +116,30 @@ export const deleteSnippet = asyncWrapper(
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Count snippets by language
|
||||
* @route /api/snippets/statistics/count
|
||||
* @request GET
|
||||
*/
|
||||
export const countSnippets = asyncWrapper(
|
||||
async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
const result = await sequelize.query(
|
||||
`SELECT
|
||||
COUNT(language) AS count,
|
||||
language
|
||||
FROM snippets
|
||||
GROUP BY language
|
||||
ORDER BY
|
||||
count DESC,
|
||||
language ASC`,
|
||||
{
|
||||
type: QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
data: result
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
SnippetCreationAttributes
|
||||
} from '../../typescript/interfaces';
|
||||
|
||||
const { INTEGER, STRING, DATE } = DataTypes;
|
||||
const { INTEGER, STRING, DATE, TEXT } = DataTypes;
|
||||
|
||||
export const up = async (queryInterface: QueryInterface): Promise<void> => {
|
||||
await queryInterface.createTable<Model<Snippet, SnippetCreationAttributes>>(
|
||||
@@ -20,10 +20,24 @@ export const up = async (queryInterface: QueryInterface): Promise<void> => {
|
||||
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
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../db';
|
||||
import { Snippet, SnippetCreationAttributes } from '../typescript/interfaces';
|
||||
|
||||
const { INTEGER, STRING, DATE } = DataTypes;
|
||||
const { INTEGER, STRING, DATE, TEXT } = DataTypes;
|
||||
|
||||
interface SnippetInstance
|
||||
extends Model<Snippet, SnippetCreationAttributes>,
|
||||
@@ -18,10 +18,24 @@ export const SnippetModel = sequelize.define<SnippetInstance>('Snippet', {
|
||||
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
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import {
|
||||
countSnippets,
|
||||
createSnippet,
|
||||
deleteSnippet,
|
||||
getAllSnippets,
|
||||
@@ -12,7 +13,7 @@ export const snippetRouter = Router();
|
||||
|
||||
snippetRouter
|
||||
.route('/')
|
||||
.post(requireBody('title', 'language'), createSnippet)
|
||||
.post(requireBody('title', 'language', 'code'), createSnippet)
|
||||
.get(getAllSnippets);
|
||||
|
||||
snippetRouter
|
||||
@@ -20,3 +21,5 @@ snippetRouter
|
||||
.get(getSnippet)
|
||||
.put(updateSnippet)
|
||||
.delete(deleteSnippet);
|
||||
|
||||
snippetRouter.route('/statistics/count').get(countSnippets);
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Optional } from 'sequelize';
|
||||
|
||||
export interface Snippet extends Model {
|
||||
title: string;
|
||||
description: string;
|
||||
language: string;
|
||||
code: string;
|
||||
docs: string;
|
||||
}
|
||||
|
||||
export interface SnippetCreationAttributes
|
||||
|
||||
Reference in New Issue
Block a user