├── .gitignore ├── README.md ├── src ├── index.coffee ├── api │ └── youtube.coffee ├── components │ ├── StyleHoc.coffee │ ├── VideoList.coffee │ ├── SearchBar.coffee │ ├── VideoDetail.coffee │ └── VideoListItem.coffee ├── App.coffee └── helpers │ └── component.coffee ├── webpack.config.js ├── package.json └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cs-react 2 | Proof of concept of neat react + coffeescript integration. 3 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | import "./helpers/component" 2 | import ReactDOM from "react-dom" 3 | import App from "./App" 4 | 5 | ReactDOM.render App(), document.querySelector '#app' 6 | -------------------------------------------------------------------------------- /src/api/youtube.coffee: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | API_KEY = 'AIzaSyB0ToJFPdt3Dp2stb8ynXqHkNe9U0ivB0c' 4 | 5 | export default (term) -> 6 | axios.get "https://www.googleapis.com/youtube/v3/search", 7 | params: 8 | key: API_KEY 9 | type: 'video' 10 | part: 'snippet' 11 | q: term 12 | -------------------------------------------------------------------------------- /src/components/StyleHoc.coffee: -------------------------------------------------------------------------------- 1 | import { Component } from "react" 2 | import wrapper from "../helpers/component" 3 | 4 | # Example of a higher order component 5 | export default (WrappedComponent) -> 6 | wrapper class extends Component 7 | render: -> 8 | WrappedComponent { @props..., style: border: "5px solid red" } 9 | -------------------------------------------------------------------------------- /src/components/VideoList.coffee: -------------------------------------------------------------------------------- 1 | import { Component } from "react" 2 | import wrapper from "../helpers/component" 3 | import VideoListItem from "./VideoListItem" 4 | 5 | VideoList = ({ videos, onVideoSelect }) -> 6 | ul 7 | className: "col-md-4 list-group" 8 | VideoListItem { video, onVideoSelect } for video in videos 9 | 10 | export default wrapper VideoList 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: './src/index.coffee', 6 | output: { 7 | path: path.resolve(__dirname), 8 | filename: 'bundle.js', 9 | publicPath: '/' 10 | }, 11 | module: { 12 | rules: [{ test: /\.coffee$/, use: 'coffee-loader' }] 13 | }, 14 | resolve: { 15 | extensions: ['.js', '.coffee'] 16 | }, 17 | devServer: { 18 | contentBase: './' 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/SearchBar.coffee: -------------------------------------------------------------------------------- 1 | import { Component } from "react" 2 | import wrapper from "../helpers/component" 3 | 4 | class SearchBar extends Component 5 | constructor: (props) -> 6 | super props 7 | 8 | @state = term: "" 9 | 10 | onInputChange: (event) => 11 | term = event.target.value 12 | @setState { term } 13 | @props.onTermChange term 14 | 15 | render: -> 16 | div 17 | style: @props.style 18 | className: "search-bar" 19 | input 20 | value: @state.term 21 | onChange: @onInputChange 22 | 23 | export default wrapper SearchBar 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "021", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^0.18.0", 13 | "coffee-loader": "^0.9.0", 14 | "coffeescript": "^2.2.4", 15 | "lodash": "^4.17.10", 16 | "react": "^16.3.2", 17 | "react-dom": "^16.3.2", 18 | "react-redux": "^5.0.7", 19 | "redux": "^4.0.0", 20 | "webpack": "^4.6.0", 21 | "webpack-cli": "^2.0.15", 22 | "webpack-dev-server": "^3.1.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/VideoDetail.coffee: -------------------------------------------------------------------------------- 1 | import { Component } from "react" 2 | import wrapper from "../helpers/component" 3 | 4 | VideoDetail = ({ video }) -> 5 | return "Loading" unless video 6 | 7 | { videoId } = video.id 8 | url = "https://www.youtube.com/embed/#{videoId}" 9 | 10 | div 11 | className: "video-detail col-md-8" 12 | div 13 | className: "embed-responsive embed-responsive-16by9" 14 | iframe 15 | className: "embed-responsive-item" 16 | src: url 17 | div 18 | className: "details" 19 | div video.snippet.title 20 | div video.snippet.description 21 | 22 | export default wrapper VideoDetail 23 | -------------------------------------------------------------------------------- /src/components/VideoListItem.coffee: -------------------------------------------------------------------------------- 1 | import { Component } from "react" 2 | import wrapper from "../helpers/component" 3 | 4 | VideoListItem = ({ video, onVideoSelect }) -> 5 | { url } = video.snippet.thumbnails.default 6 | 7 | li 8 | onClick: -> onVideoSelect(video) 9 | className: "list-group-item" 10 | div 11 | className: "video-list media" 12 | div 13 | className: "media-left" 14 | img 15 | className: "media-object" 16 | src: url 17 | div 18 | className: "media-body" 19 | div 20 | className: "media-heading" 21 | video.snippet.title 22 | 23 | export default wrapper VideoListItem 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/App.coffee: -------------------------------------------------------------------------------- 1 | import { Component } from "react" 2 | import debounce from "lodash/debounce" 3 | import wrapper from "./helpers/component" 4 | import search from "./api/youtube" 5 | import VideoList from "./components/VideoList" 6 | import VideoDetail from "./components/VideoDetail" 7 | import SearchBar from "./components/SearchBar" 8 | 9 | class App extends Component 10 | constructor: (props) -> 11 | super props 12 | 13 | @state = videos: [], selectedVideo: null 14 | @videoSearch = debounce @videoSearch, 300 15 | 16 | componentDidMount: -> 17 | @videoSearch "surfboards" 18 | 19 | videoSearch: (term) => 20 | res = await search term 21 | @setState videos: res.data.items, selectedVideo: res.data.items[0] 22 | 23 | onVideoSelect: (video) => 24 | @setState selectedVideo: video 25 | 26 | render: -> 27 | div 28 | className: "container" 29 | SearchBar onTermChange: @videoSearch 30 | VideoDetail video: @state.selectedVideo 31 | VideoList { videos: @state.videos, @onVideoSelect } 32 | 33 | export default wrapper App 34 | -------------------------------------------------------------------------------- /src/helpers/component.coffee: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import isPlainObject from 'lodash/isPlainObject' 3 | import isArray from 'lodash/isArray' 4 | 5 | wrapper = (component) -> (specs...) -> 6 | conf = {} 7 | 8 | # Capture component props here. Each component is assumed 9 | # to have single props input. $$typeof check excludes vanilla HTML elements 10 | for spec in specs when isPlainObject(spec) and not spec.$$typeof 11 | conf = spec 12 | 13 | # Capture children. $$typeof check includes vanilla html elements 14 | children = (spec for spec in specs when not isPlainObject(spec) or spec.$$typeof) 15 | 16 | if isArray children... 17 | React.createFactory(component).bind(null, conf).apply(null, children...) 18 | else 19 | React.createFactory(component)(conf, children...) 20 | 21 | elements = ["a", "article", "b", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "details", "dialog", "div", "em", "embed", "fieldset", "footer", "form", "frame", "frameset", "h1", "head", "header", "hr", "html", "i", "iframe", "image", "img", "input", "isindex", "label", "legend", "li", "link", "listing", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "multicol", "nav", "nextid", "nobr", "noembed", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "plaintext", "pre", "progress", "q", "rp", "rt", "rtc", "ruby", "s", "samp", "script", "section", "select", "shadow", "slot", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr", "xmp"] 22 | 23 | for element in elements 24 | window[element] = wrapper element 25 | 26 | export default wrapper 27 | --------------------------------------------------------------------------------