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 { |
export interface ClassDictionary { |
||||
[name: string]: boolean |
[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