Dwayne Harris
5 years ago
18 changed files with 747 additions and 93 deletions
-
283package-lock.json
-
13package.json
-
163src/apps/gif-app/composer/app.tsx
-
24src/apps/gif-app/composer/gif.tsx
-
BINsrc/apps/gif-app/composer/giphy.png
-
12src/apps/gif-app/composer/index.ejs
-
6src/apps/gif-app/composer/index.tsx
-
63src/apps/gif-app/composer/webpack.config.ts
-
17src/apps/text-app/composer/app.tsx
-
2src/apps/text-app/composer/index.ejs
-
4src/apps/text-app/composer/index.tsx
-
1src/apps/text-app/composer/webpack.config.ts
-
16src/communicator/index.ts
-
145src/server/api/index.ts
-
14src/server/index.ts
-
17src/styles/default.scss
-
58src/types/index.ts
-
2tsconfig.json
@ -0,0 +1,163 @@ |
|||
import React, { FC, useState, useEffect } from 'react' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faSearch } from '@fortawesome/free-solid-svg-icons' |
|||
import { Communicator } from '../../../communicator' |
|||
import classNames from 'classnames' |
|||
import Gif from './gif' |
|||
import { ClassDictionary, GiphyGif } from '../../../types' |
|||
|
|||
const giphyIcon = require('./giphy.png') |
|||
import '../../../styles/default.scss' |
|||
|
|||
type APIResponse = GiphyGif[] |
|||
|
|||
interface Props { |
|||
communicator: Communicator |
|||
} |
|||
|
|||
const useDebounce = (value: string, delay = 500) => { |
|||
const [valueD, setValueD] = useState(value) |
|||
|
|||
useEffect(() => { |
|||
const handler = setTimeout(() => { |
|||
setValueD(value) |
|||
}, delay) |
|||
|
|||
return () => { |
|||
clearTimeout(handler) |
|||
} |
|||
}, [value, delay]) |
|||
|
|||
return valueD |
|||
} |
|||
|
|||
const App: FC<Props> = ({ communicator }) => { |
|||
const [posting, setPosting] = useState(false) |
|||
const [searching, setSearching] = useState(false) |
|||
const [search, setSearch] = useState('') |
|||
const searchD = useDebounce(search) |
|||
const [gifs, setGifs] = useState<GiphyGif[]>([]) |
|||
const [selected, setSelected] = useState('') |
|||
|
|||
const buttonStyle: ClassDictionary = { |
|||
'button': true, |
|||
'is-primary': true, |
|||
'is-loading': posting, |
|||
} |
|||
|
|||
const controlStyle: ClassDictionary = { |
|||
'control': true, |
|||
'has-icons-left': true, |
|||
'is-loading': searching, |
|||
} |
|||
|
|||
useEffect(() => { |
|||
const init = async () => { |
|||
try { |
|||
const content = await communicator.init() |
|||
if (content && content.parent && content.parent.data && content.parent.data.search) { |
|||
setSearch(content.parent.data.search) |
|||
} |
|||
|
|||
await communicator.setHeight(1000) |
|||
} catch (err) { |
|||
console.error(err) |
|||
} |
|||
} |
|||
|
|||
init() |
|||
}, []) |
|||
|
|||
useEffect(() => { |
|||
const doTrending = async () => { |
|||
try { |
|||
const response = await fetch('/api/gifs/home') |
|||
setGifs(await response.json() as APIResponse) |
|||
} catch (err) { |
|||
console.error(err) |
|||
} |
|||
} |
|||
|
|||
const doSearch = async () => { |
|||
try { |
|||
setSearching(true) |
|||
const response = await fetch(`/api/gifs/search?q=${search}`) |
|||
setGifs(await response.json() as APIResponse) |
|||
} catch (err) { |
|||
console.error(err) |
|||
} |
|||
|
|||
setSearching(false) |
|||
} |
|||
|
|||
if (!search) { |
|||
doTrending() |
|||
} else if (search.length > 2) { |
|||
doSearch() |
|||
} |
|||
}, [searchD]) |
|||
|
|||
const handleSearchChange = (search: string) => { |
|||
setSearch(search) |
|||
} |
|||
|
|||
const post = async () => { |
|||
try { |
|||
const gif = gifs.find(g => g.id === selected) |
|||
if (!gif) return |
|||
|
|||
setPosting(true) |
|||
|
|||
await communicator.post({ |
|||
attachments: [{ |
|||
url: gif.images.fixed_height.url, |
|||
text: gif.title, |
|||
}], |
|||
data: { |
|||
search, |
|||
}, |
|||
visible: true, |
|||
}) |
|||
|
|||
setSearch('') |
|||
setSelected('') |
|||
} catch (err) { |
|||
console.error(err) |
|||
} |
|||
|
|||
setPosting(false) |
|||
} |
|||
|
|||
return ( |
|||
<div> |
|||
<div className="field"> |
|||
<p className={classNames(controlStyle)}> |
|||
<input className="input" type="text" placeholder="Search" value={search} onChange={(e) => handleSearchChange(e.target.value)} /> |
|||
<span className="icon is-small is-left"> |
|||
<FontAwesomeIcon icon={faSearch} /> |
|||
</span> |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="gifs is-flex"> |
|||
{gifs.map(gif => <Gif key={gif.id} gif={gif} selected={gif.id === selected} onSelect={setSelected} />)} |
|||
</div> |
|||
|
|||
<nav className="level"> |
|||
<div className="level-left"> |
|||
<div className="level-item"> |
|||
<img src={giphyIcon} /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="level-right"> |
|||
<div className="level-item"> |
|||
<button className={classNames(buttonStyle)} onClick={() => post()} disabled={!selected}>Post</button> |
|||
</div> |
|||
</div> |
|||
</nav> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default App |
@ -0,0 +1,24 @@ |
|||
import React, { FC, useState, useEffect } from 'react' |
|||
import classNames from 'classnames' |
|||
import { ClassDictionary, GiphyGif } from '../../../types' |
|||
|
|||
interface Props { |
|||
gif: GiphyGif |
|||
selected: boolean |
|||
onSelect: (id: string) => void |
|||
} |
|||
|
|||
const Gif: FC<Props> = ({ gif, selected, onSelect }) => { |
|||
const classes: ClassDictionary = { |
|||
gif: true, |
|||
selected, |
|||
} |
|||
|
|||
return ( |
|||
<div className={classNames(classes)} onClick={() => onSelect(gif.id)}> |
|||
<img src={gif.images.fixed_height.url} alt={gif.title} /> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default Gif |
After Width: 101 | Height: 36 | Size: 1.6 KiB |
@ -0,0 +1,12 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<title>GIF App</title> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
</head> |
|||
|
|||
<body> |
|||
<div id="app"></div> |
|||
</body> |
|||
</html> |
@ -0,0 +1,6 @@ |
|||
import React from 'react' |
|||
import { render } from 'react-dom' |
|||
import App from './app' |
|||
import { Communicator } from '../../../communicator' |
|||
|
|||
render(<App communicator={new Communicator('e396113e76dffaa2ad6c')} />, document.getElementById('app')) |
@ -0,0 +1,63 @@ |
|||
import { resolve } from 'path' |
|||
import { Configuration } from 'webpack' |
|||
import HtmlWebpackPlugin from 'html-webpack-plugin' |
|||
import MiniCssExtractPlugin from 'mini-css-extract-plugin' |
|||
|
|||
const config: Configuration = { |
|||
mode: 'development', |
|||
devtool: 'eval-source-map', |
|||
entry: { |
|||
app: resolve(__dirname, './index.tsx'), |
|||
}, |
|||
output: { |
|||
path: resolve(__dirname, '../../../../dist/apps/gif-app/'), |
|||
filename: '[name].js', |
|||
}, |
|||
optimization: { |
|||
splitChunks: { |
|||
chunks: 'all', |
|||
}, |
|||
}, |
|||
resolve: { |
|||
extensions: ['.ts', '.tsx', '.js', '.png'], |
|||
}, |
|||
module: { |
|||
rules: [ |
|||
{ |
|||
test: /\.ts(x?)$/, |
|||
exclude: /node_modules/, |
|||
use: 'ts-loader', |
|||
}, |
|||
{ |
|||
test: /\.scss$/, |
|||
use: [ |
|||
MiniCssExtractPlugin.loader, |
|||
'css-loader', |
|||
{ |
|||
loader: 'sass-loader', |
|||
options: { |
|||
sourceMap: true, |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
test: /\.(jpe?g|gif|png|svg)$/, |
|||
use: ['file-loader'], |
|||
}, |
|||
], |
|||
}, |
|||
plugins: [ |
|||
new HtmlWebpackPlugin({ |
|||
title: 'GIF App', |
|||
hash: true, |
|||
template: resolve(__dirname, './index.ejs'), |
|||
filename: 'composer.html', |
|||
}), |
|||
new MiniCssExtractPlugin({ |
|||
filename: '[name].css', |
|||
}), |
|||
], |
|||
} |
|||
|
|||
export default config |
@ -0,0 +1,145 @@ |
|||
import { |
|||
FastifyInstance, |
|||
RouteShorthandOptions, |
|||
Plugin, |
|||
DefaultQuery, |
|||
DefaultParams, |
|||
DefaultHeaders, |
|||
DefaultBody, |
|||
JSONSchema, |
|||
} from 'fastify' |
|||
import { Server, IncomingMessage, ServerResponse } from 'http' |
|||
import request from 'request' |
|||
import { PluginOptions, GiphyResponse } from '../../types' |
|||
|
|||
interface FetchOptions { |
|||
url: string |
|||
params?: string[][] |
|||
method?: string |
|||
} |
|||
|
|||
const paramsToString = (params: string[][]) => params.map(p => p.join('=')).join('&') |
|||
|
|||
const giphyGifSchema: JSONSchema = { |
|||
type: 'object', |
|||
properties: { |
|||
type: { type: 'string' }, |
|||
id: { type: 'string' }, |
|||
slug: { type: 'string' }, |
|||
url: { type: 'string' }, |
|||
bitly_url: { type: 'string' }, |
|||
embed_url: { type: 'string' }, |
|||
title: { type: 'string' }, |
|||
images: { |
|||
type: 'object', |
|||
properties: { |
|||
fixed_height: { |
|||
type: 'object', |
|||
properties: { |
|||
url: { type: 'string' }, |
|||
}, |
|||
}, |
|||
fixed_height_still: { |
|||
type: 'object', |
|||
properties: { |
|||
url: { type: 'string' }, |
|||
}, |
|||
}, |
|||
fixed_height_downsampled: { |
|||
type: 'object', |
|||
properties: { |
|||
url: { type: 'string' }, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
async function fetch<T = any>(options: FetchOptions) { |
|||
const { url, params = [], method = 'get' } = options |
|||
const uri = params.length > 0 ? `${url}?${paramsToString(params)}` : url |
|||
|
|||
return new Promise<T>((resolve, reject) => { |
|||
request({ |
|||
uri, |
|||
method, |
|||
json: true, |
|||
}, function(error, response, body) { |
|||
if (error) { |
|||
reject(error) |
|||
return |
|||
} |
|||
|
|||
resolve(body) |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
function gifHomeRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
|||
const options: RouteShorthandOptions = { |
|||
schema: { |
|||
response: { |
|||
200: { |
|||
type: 'array', |
|||
items: giphyGifSchema, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
server.get<DefaultQuery, DefaultParams, DefaultHeaders, DefaultBody>('/api/gifs/home', options, async (request, reply) => { |
|||
const response = await fetch<GiphyResponse>({ |
|||
url: `${process.env.GIPHY_API_ENDPOINT!}/v1/gifs/trending`, |
|||
params: [['api_key', process.env.GIPHY_API_KEY!], ['limit', '20']], |
|||
}) |
|||
|
|||
return response.data |
|||
}) |
|||
} |
|||
|
|||
function gifSearchRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
|||
interface Query { |
|||
q: string |
|||
} |
|||
|
|||
const options: RouteShorthandOptions = { |
|||
schema: { |
|||
querystring: { |
|||
type: 'object', |
|||
properties: { |
|||
q: { type: 'string' }, |
|||
}, |
|||
}, |
|||
response: { |
|||
200: { |
|||
type: 'array', |
|||
items: giphyGifSchema, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/api/gifs/search', options, async (request, reply) => { |
|||
const q = request.query.q ? request.query.q.trim() : '' |
|||
|
|||
if (q.length < 3) { |
|||
reply.code(400) |
|||
return |
|||
} |
|||
|
|||
const response = await fetch<GiphyResponse>({ |
|||
url: `${process.env.GIPHY_API_ENDPOINT!}/v1/gifs/search`, |
|||
params: [['api_key', process.env.GIPHY_API_KEY!], ['limit', '20'], ['q', q]], |
|||
}) |
|||
|
|||
return response.data |
|||
}) |
|||
} |
|||
|
|||
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => { |
|||
gifHomeRoute(server) |
|||
gifSearchRoute(server) |
|||
} |
|||
|
|||
export default plugin |
@ -1,3 +1,61 @@ |
|||
export interface ClassDictionary { |
|||
[name: string]: boolean |
|||
} |
|||
|
|||
export interface PluginOptions { |
|||
|
|||
} |
|||
|
|||
export interface Attachment { |
|||
url: string |
|||
text?: string |
|||
cover?: string |
|||
} |
|||
|
|||
export interface PostData { |
|||
[key: string]: any |
|||
} |
|||
|
|||
export interface Post { |
|||
text?: string |
|||
cover?: string |
|||
attachments?: Attachment[] |
|||
data?: PostData |
|||
visible: boolean |
|||
} |
|||
|
|||
export interface GiphyGif { |
|||
type: string |
|||
id: string |
|||
slug: string |
|||
url: string |
|||
bitly_url: string |
|||
embed_url: string |
|||
username: string |
|||
source: string |
|||
rating: string |
|||
content_url: string |
|||
title: string |
|||
images: { |
|||
fixed_height: { |
|||
url: string |
|||
} |
|||
fixed_height_still: { |
|||
url: string |
|||
} |
|||
fixed_height_downsampled: { |
|||
url: string |
|||
} |
|||
} |
|||
} |
|||
|
|||
export interface GiphyPagination { |
|||
offset: number |
|||
total_count: number |
|||
count: number |
|||
} |
|||
|
|||
export interface GiphyResponse { |
|||
data: GiphyGif[] |
|||
pagination: GiphyPagination |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue