├── .gitignore ├── .idea ├── kotlin-react-sample.iml ├── kotlinc.xml ├── libraries │ ├── KotlinJavaScript.xml │ ├── kotlin_extensions.xml │ ├── kotlin_react.xml │ ├── kotlin_react_dom.xml │ ├── kotlinx_coroutines_core.xml │ └── kotlinx_html_js.xml ├── misc.xml ├── modules.xml ├── runConfigurations │ ├── Debug_in_Chrome.xml │ └── npm_start.xml ├── vcs.xml └── workspace.xml ├── LICENSE ├── README.md ├── docs ├── asset-manifest.json ├── favicon.ico ├── index.html ├── manifest.json └── static │ ├── css │ ├── main.0aea1c60.css │ └── main.0aea1c60.css.map │ └── js │ ├── main.9a137a24.js │ └── main.9a137a24.js.map ├── kotlin-react-sample.iml ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── app ├── App.css └── App.kt ├── axios └── Axios.kt ├── common ├── FunComp.kt ├── SearchBar.kt ├── funcomp.css └── search_bar.css ├── giphy ├── GiphyAPI.kt ├── GiphyDetails.kt ├── GiphyList.kt ├── GiphyListItem.kt └── giphy.css ├── index ├── index.css └── index.kt └── lodash └── Lodash.kt /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | .idea/workspace.xml 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | -------------------------------------------------------------------------------- /.idea/kotlin-react-sample.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/libraries/KotlinJavaScript.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/kotlin_extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/libraries/kotlin_react.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/kotlin_react_dom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/kotlinx_coroutines_core.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/libraries/kotlinx_html_js.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Debug_in_Chrome.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/runConfigurations/npm_start.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Giphy React Kotlin App", 3 | "name": "Giphy React Kotlin App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "/index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /docs/static/css/main.0aea1c60.css: -------------------------------------------------------------------------------- 1 | body{text-align:center}.App-header{background-color:#000;height:160px;padding:20px;color:#fff}.spinner{display:inline-block;border:.2em solid #f3f3f3;border-top:.2em solid #3498db;border-radius:50%;width:1em;height:1em;-webkit-animation:spin 2s linear infinite;animation:spin 2s linear infinite}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.search-bar{margin:20px;text-align:center}.search-bar input{width:75%}.giphy-item img{max-width:80%}.giphy-detail .details{margin:10px 0}.list-group-item{cursor:pointer}.list-group-item:hover{background-color:#eee}body{margin:0;padding:0;font-family:sans-serif} 2 | /*# sourceMappingURL=main.0aea1c60.css.map*/ -------------------------------------------------------------------------------- /docs/static/css/main.0aea1c60.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["app/App.css","common/funcomp.css","common/search_bar.css","giphy/giphy.css","index/index.css"],"names":[],"mappings":"AAAA,KACI,iBAAmB,CAGvB,YACI,sBACA,aACA,aACA,UAAa,CCPjB,SACI,qBACA,0BACA,8BACA,kBACA,UACA,WACA,0CACQ,iCAAmC,CAG/C,wBACI,GACI,+BACQ,sBAAwB,CAEpC,GACI,gCACQ,uBAA0B,CACrC,CAGL,gBACI,GACI,+BACQ,sBAAwB,CAEpC,GACI,gCACQ,uBAA0B,CACrC,CC9BL,YACI,YACA,iBAAmB,CAGvB,kBACI,SAAW,CCNf,gBACI,aAAe,CAGnB,uBACI,aAAsB,CAG1B,iBACI,cAAgB,CAGpB,uBACI,qBAAuB,CCd3B,KACI,SACA,UACA,sBAAwB","file":"static/css/main.0aea1c60.css","sourcesContent":["body {\n text-align: center;\n}\n\n.App-header {\n background-color: #000;\n height: 160px;\n padding: 20px;\n color: white;\n}\n\n\n\n\n// WEBPACK FOOTER //\n// ./src/app/App.css","\n.spinner {\n display: inline-block;\n border: 0.2em solid #f3f3f3;\n border-top: 0.2em solid #3498db;\n border-radius: 50%;\n width: 1em;\n height: 1em;\n -webkit-animation: spin 2s linear infinite;\n animation: spin 2s linear infinite;\n}\n\n@-webkit-keyframes spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n@keyframes spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n\n// WEBPACK FOOTER //\n// ./src/common/funcomp.css","\n.search-bar {\n margin: 20px;\n text-align: center;\n}\n\n.search-bar input {\n width: 75%;\n}\n\n\n\n\n// WEBPACK FOOTER //\n// ./src/common/search_bar.css","\n.giphy-item img {\n max-width: 80%;\n}\n\n.giphy-detail .details {\n margin: 10px 0 10px 0;\n}\n\n.list-group-item {\n cursor: pointer;\n}\n\n.list-group-item:hover {\n background-color: #eee;\n}\n\n\n\n\n// WEBPACK FOOTER //\n// ./src/giphy/giphy.css","body {\n margin: 0;\n padding: 0;\n font-family: sans-serif;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/index/index.css"],"sourceRoot":""} -------------------------------------------------------------------------------- /kotlin-react-sample.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kotlin-react-sample", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.18.0", 7 | "kotlinx-coroutines-core": "^0.22.5", 8 | "lodash": "^4.17.10", 9 | "react": "^16.3.2", 10 | "react-bootstrap": "^0.32.1", 11 | "react-dom": "^16.3.2" 12 | }, 13 | "devDependencies": { 14 | "react-scripts-kotlin": "2.1.9" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts-kotlin start", 18 | "build": "react-scripts-kotlin build", 19 | "eject": "react-scripts-kotlin eject", 20 | "gen-idea-libs": "react-scripts-kotlin gen-idea-libs", 21 | "get-types": "react-scripts-kotlin get-types --dest=src/types", 22 | "postinstall": "npm run gen-idea-libs" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ralfstuckert/kotlin-react-sample/3e17e4cbe06cdccf4b90e7be90523819373d49a5/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | 24 | 25 | React Kotlin App 26 | 27 | 28 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Giphy React Kotlin App", 3 | "name": "Giphy React Kotlin App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "/index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/app/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | text-align: center; 3 | } 4 | 5 | .App-header { 6 | background-color: #000; 7 | height: 160px; 8 | padding: 20px; 9 | color: white; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/app/App.kt: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import common.alert 4 | import common.loading 5 | import common.searchBar 6 | import giphy.* 7 | import kotlinx.coroutines.experimental.async 8 | import lodash.lodash 9 | import react.* 10 | import react.dom.div 11 | import react.dom.h1 12 | 13 | 14 | interface AppState : RState { 15 | var giphies: Array 16 | var selectedGiphy: Giphy 17 | var errorMessage: String 18 | var loading: Boolean 19 | } 20 | 21 | 22 | class App : RComponent() { 23 | 24 | override fun AppState.init() { 25 | giphies = emptyArray() 26 | selectedGiphy = DummyGiphy 27 | errorMessage = "" 28 | loading = false 29 | } 30 | 31 | override fun RBuilder.render() { 32 | div("container") { 33 | h1("text-center") { 34 | +"Giphy Search" 35 | } 36 | searchBar(onSearchTermChange = lodash.debounce(::startSearchCoroutines, 300)) 37 | alert(state.errorMessage) 38 | loading(state.loading) 39 | giphyDetails(state.selectedGiphy) 40 | giphyList(state.giphies, ::selectGiphy) 41 | } 42 | } 43 | 44 | fun selectGiphy(giphy: Giphy) { 45 | setState { 46 | selectedGiphy = giphy 47 | } 48 | } 49 | 50 | fun startSearch(term: String) { 51 | setState { 52 | loading = true 53 | errorMessage = "" 54 | } 55 | 56 | giphySearch(term).then { result: Array -> 57 | setState { 58 | selectedGiphy = result[0] 59 | giphies = result 60 | loading = false 61 | } 62 | }.catch { throwable: Throwable -> 63 | setState { 64 | errorMessage = throwable.toString() 65 | loading = false 66 | } 67 | } 68 | } 69 | 70 | fun startSearchCoroutines(term: String) = async { 71 | setState { 72 | loading = true 73 | errorMessage = "" 74 | } 75 | 76 | try { 77 | val result: Array = giphySearchCoroutines(term) 78 | setState { 79 | selectedGiphy = result[0] 80 | giphies = result 81 | loading = false 82 | } 83 | } catch (t: Throwable) { 84 | setState { 85 | errorMessage = t.toString() 86 | loading = false 87 | } 88 | } 89 | } 90 | 91 | } 92 | 93 | fun RBuilder.app() = child(App::class) {} 94 | -------------------------------------------------------------------------------- /src/axios/Axios.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS", "EXTERNAL_DELEGATION", "NESTED_CLASS_IN_EXTERNAL_INTERFACE") 2 | 3 | package axios 4 | 5 | import kotlin.js.Promise 6 | 7 | external interface AxiosTransformer { 8 | @nativeInvoke 9 | operator fun invoke(data: Any, headers: Any? = definedExternally /* null */): Any 10 | } 11 | external interface AxiosAdapter { 12 | @nativeInvoke 13 | operator fun invoke(config: AxiosRequestConfig): AxiosPromise 14 | } 15 | external interface AxiosBasicCredentials { 16 | var username: String 17 | var password: String 18 | } 19 | external interface `T$0` { 20 | var username: String 21 | var password: String 22 | } 23 | external interface AxiosProxyConfig { 24 | var host: String 25 | var port: Number 26 | var auth: `T$0`? get() = definedExternally; set(value) = definedExternally 27 | } 28 | external interface AxiosRequestConfig { 29 | var url: String? get() = definedExternally; set(value) = definedExternally 30 | var method: String? get() = definedExternally; set(value) = definedExternally 31 | var baseURL: String? get() = definedExternally; set(value) = definedExternally 32 | var transformRequest: dynamic /* AxiosTransformer | Array */ get() = definedExternally; set(value) = definedExternally 33 | var transformResponse: dynamic /* AxiosTransformer | Array */ get() = definedExternally; set(value) = definedExternally 34 | var headers: Any? get() = definedExternally; set(value) = definedExternally 35 | var params: Any? get() = definedExternally; set(value) = definedExternally 36 | var paramsSerializer: ((params: Any) -> String)? get() = definedExternally; set(value) = definedExternally 37 | var data: Any? get() = definedExternally; set(value) = definedExternally 38 | var timeout: Number? get() = definedExternally; set(value) = definedExternally 39 | var withCredentials: Boolean? get() = definedExternally; set(value) = definedExternally 40 | var adapter: AxiosAdapter? get() = definedExternally; set(value) = definedExternally 41 | var auth: AxiosBasicCredentials? get() = definedExternally; set(value) = definedExternally 42 | var responseType: String? get() = definedExternally; set(value) = definedExternally 43 | var xsrfCookieName: String? get() = definedExternally; set(value) = definedExternally 44 | var xsrfHeaderName: String? get() = definedExternally; set(value) = definedExternally 45 | var onUploadProgress: ((progressEvent: Any) -> Unit)? get() = definedExternally; set(value) = definedExternally 46 | var onDownloadProgress: ((progressEvent: Any) -> Unit)? get() = definedExternally; set(value) = definedExternally 47 | var maxContentLength: Number? get() = definedExternally; set(value) = definedExternally 48 | var validateStatus: ((status: Number) -> Boolean)? get() = definedExternally; set(value) = definedExternally 49 | var maxRedirects: Number? get() = definedExternally; set(value) = definedExternally 50 | var httpAgent: Any? get() = definedExternally; set(value) = definedExternally 51 | var httpsAgent: Any? get() = definedExternally; set(value) = definedExternally 52 | var proxy: dynamic /* Boolean | AxiosProxyConfig */ get() = definedExternally; set(value) = definedExternally 53 | var cancelToken: CancelToken? get() = definedExternally; set(value) = definedExternally 54 | } 55 | external interface AxiosResponse { 56 | var data: T 57 | var status: Number 58 | var statusText: String 59 | var headers: Any 60 | var config: AxiosRequestConfig 61 | var request: Any? get() = definedExternally; set(value) = definedExternally 62 | } 63 | 64 | external interface Error 65 | 66 | external interface AxiosError : Error { 67 | var config: AxiosRequestConfig 68 | var code: String? get() = definedExternally; set(value) = definedExternally 69 | var request: Any? get() = definedExternally; set(value) = definedExternally 70 | var response: AxiosResponse? get() = definedExternally; set(value) = definedExternally 71 | } 72 | external interface AxiosPromise : Promise> 73 | external interface CancelStatic 74 | external interface Cancel { 75 | var message: String 76 | } 77 | external interface Canceler { 78 | @nativeInvoke 79 | operator fun invoke(message: String? = definedExternally /* null */) 80 | } 81 | external interface CancelTokenStatic { 82 | fun source(): CancelTokenSource 83 | } 84 | external interface CancelToken { 85 | var promise: Promise 86 | var reason: Cancel? get() = definedExternally; set(value) = definedExternally 87 | fun throwIfRequested() 88 | } 89 | external interface CancelTokenSource { 90 | var token: CancelToken 91 | var cancel: Canceler 92 | } 93 | external interface AxiosInterceptorManager { 94 | fun use(onFulfilled: ((value: V) -> dynamic /* V | Promise */)? = definedExternally /* null */, onRejected: ((error: Any) -> Any)? = definedExternally /* null */): Number 95 | fun eject(id: Number) 96 | } 97 | external interface `T$1` { 98 | var request: AxiosInterceptorManager 99 | var response: AxiosInterceptorManager> 100 | } 101 | external interface AxiosInstance { 102 | @nativeInvoke 103 | operator fun invoke(config: AxiosRequestConfig): AxiosPromise 104 | @nativeInvoke 105 | operator fun invoke(url: String, config: AxiosRequestConfig? = definedExternally /* null */): AxiosPromise 106 | var defaults: AxiosRequestConfig 107 | var interceptors: `T$1` 108 | fun request(config: AxiosRequestConfig): AxiosPromise 109 | fun get(url: String, config: AxiosRequestConfig? = definedExternally /* null */): AxiosPromise 110 | fun delete(url: String, config: AxiosRequestConfig? = definedExternally /* null */): AxiosPromise 111 | fun head(url: String, config: AxiosRequestConfig? = definedExternally /* null */): AxiosPromise 112 | fun post(url: String, data: Any? = definedExternally /* null */, config: AxiosRequestConfig? = definedExternally /* null */): AxiosPromise 113 | fun put(url: String, data: Any? = definedExternally /* null */, config: AxiosRequestConfig? = definedExternally /* null */): AxiosPromise 114 | fun patch(url: String, data: Any? = definedExternally /* null */, config: AxiosRequestConfig? = definedExternally /* null */): AxiosPromise 115 | } 116 | external interface AxiosStatic : AxiosInstance { 117 | fun create(config: AxiosRequestConfig? = definedExternally /* null */): AxiosInstance 118 | var Cancel: CancelStatic 119 | var CancelToken: CancelTokenStatic 120 | fun isCancel(value: Any): Boolean 121 | fun all(values: Array */>): Promise> 122 | fun spread(callback: (args: T) -> R): (array: Array) -> R 123 | } 124 | 125 | @JsModule("axios") 126 | //@JsName("default") 127 | external val Axios: AxiosStatic = definedExternally 128 | 129 | -------------------------------------------------------------------------------- /src/common/FunComp.kt: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import react.RBuilder 4 | import react.RProps 5 | import react.ReactElement 6 | import react.dom.div 7 | import react.dom.h3 8 | 9 | fun RBuilder.alert(message: String = "") = if (message.isNotEmpty()) { 10 | div("alert alert-danger") { 11 | +message 12 | } 13 | } else { 14 | empty 15 | } 16 | 17 | 18 | fun RBuilder.loading(isLoading: Boolean) = if (isLoading) { 19 | h3 { 20 | +"Loading... " 21 | spinner() 22 | } 23 | } else { 24 | empty 25 | } 26 | 27 | 28 | fun RBuilder.spinner() = div("spinner") {} 29 | 30 | 31 | object empty : ReactElement { 32 | override val props = object : RProps {} 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/common/SearchBar.kt: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import kotlinx.html.InputType 4 | import kotlinx.html.js.onChangeFunction 5 | import org.w3c.dom.HTMLInputElement 6 | import org.w3c.dom.events.Event 7 | import react.* 8 | import react.dom.div 9 | import react.dom.input 10 | 11 | interface SearchBarProps : RProps { 12 | var onSearchTermChange: (String) -> Any 13 | } 14 | 15 | interface SearchBarState : RState { 16 | var term: String 17 | } 18 | 19 | class SearchBar(props: SearchBarProps) : RComponent(props) { 20 | 21 | override fun SearchBarState.init(props: SearchBarProps) { 22 | term = "" 23 | } 24 | 25 | override fun RBuilder.render() { 26 | div("search-bar") { 27 | input(InputType.search) { 28 | attrs { 29 | value = state.term 30 | onChangeFunction = ::onInputChange 31 | placeholder = "enter search term" 32 | } 33 | } 34 | } 35 | } 36 | 37 | fun onInputChange(event: Event) { 38 | val target = event.target as HTMLInputElement 39 | val searchTerm = target.value 40 | setState { 41 | term = searchTerm 42 | } 43 | this.props.onSearchTermChange(searchTerm) 44 | } 45 | } 46 | 47 | 48 | fun RBuilder.searchBar(onSearchTermChange: (String) -> Any) = child(SearchBar::class) { 49 | attrs.onSearchTermChange = onSearchTermChange 50 | } 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/common/funcomp.css: -------------------------------------------------------------------------------- 1 | 2 | .spinner { 3 | display: inline-block; 4 | border: 0.2em solid #f3f3f3; 5 | border-top: 0.2em solid #3498db; 6 | border-radius: 50%; 7 | width: 1em; 8 | height: 1em; 9 | animation: spin 2s linear infinite; 10 | } 11 | 12 | @keyframes spin { 13 | 0% { 14 | transform: rotate(0deg); 15 | } 16 | 100% { 17 | transform: rotate(360deg); 18 | } 19 | } -------------------------------------------------------------------------------- /src/common/search_bar.css: -------------------------------------------------------------------------------- 1 | 2 | .search-bar { 3 | margin: 20px; 4 | text-align: center; 5 | } 6 | 7 | .search-bar input { 8 | width: 75%; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/giphy/GiphyAPI.kt: -------------------------------------------------------------------------------- 1 | package giphy 2 | 3 | import axios.Axios 4 | import kotlinx.coroutines.experimental.await 5 | import kotlin.js.Promise 6 | 7 | // some kotlin interfaces for the giphy API 8 | external interface GiphyImg { 9 | var url: String 10 | } 11 | 12 | external interface GiphyImgContainer { 13 | var original: GiphyImg 14 | var fixed_height_small_still: GiphyImg 15 | } 16 | 17 | external interface Giphy { 18 | var id: String 19 | var embed_url: String 20 | var images: GiphyImgContainer 21 | var slug: String 22 | } 23 | 24 | // some extensions for our application 25 | val Giphy.fileName: String 26 | get() = "${this.slug}.gif" 27 | 28 | val Giphy.giphyUrl: String 29 | get() = this.embed_url 30 | 31 | val Giphy.downloadUrl: String 32 | get() = "https://media.giphy.com/media/${this.id}/giphy.gif" 33 | 34 | val Giphy.previewUrl: String 35 | get() = this.images.fixed_height_small_still.url; 36 | 37 | 38 | object DummyGiphyImg : GiphyImg { 39 | override var url: String = "" 40 | } 41 | 42 | object DummyGiphyImgContainer : GiphyImgContainer { 43 | override var original: GiphyImg = DummyGiphyImg 44 | override var fixed_height_small_still: GiphyImg = DummyGiphyImg 45 | } 46 | 47 | object DummyGiphy : Giphy { 48 | override var id: String = "" 49 | override var embed_url: String = "" 50 | override var images: GiphyImgContainer = DummyGiphyImgContainer 51 | override var slug: String = "" 52 | } 53 | 54 | /** 55 | * Do not use for production, see https://giphy.api-docs.io/1.0/welcome/access-and-api-keys 56 | */ 57 | val PUBLIC_BETA_API_KEY = "dc6zaTOxFJmzC" 58 | 59 | val GIPHY_SEARCH = "https://api.giphy.com/v1/gifs/search" 60 | 61 | data class GiphySearchResponse(val data:Array) 62 | 63 | fun giphyUrl(searchTerm: String) = "${GIPHY_SEARCH}?q=${searchTerm}&limit=7&api_key=${PUBLIC_BETA_API_KEY}" 64 | 65 | fun giphySearch(searchTerm: String):Promise> { 66 | 67 | return Axios.get(giphyUrl(searchTerm)).then { result -> 68 | result.data.data 69 | } 70 | } 71 | 72 | suspend fun giphySearchCoroutines(searchTerm: String): Array { 73 | 74 | val result = Axios.get(giphyUrl(searchTerm)).await() 75 | return result.data.data 76 | } 77 | -------------------------------------------------------------------------------- /src/giphy/GiphyDetails.kt: -------------------------------------------------------------------------------- 1 | package giphy 2 | 3 | import kotlinx.html.role 4 | import kotlinx.html.title 5 | import react.RBuilder 6 | import react.RComponent 7 | import react.RProps 8 | import react.RState 9 | import react.dom.a 10 | import react.dom.div 11 | import react.dom.iframe 12 | 13 | 14 | interface GiphyProps : RProps { 15 | var giphy: Giphy 16 | } 17 | 18 | 19 | class GiphyDetails(props: GiphyProps) : RComponent(props) { 20 | 21 | override fun RBuilder.render() { 22 | val giphy = props.giphy 23 | 24 | if (giphy != DummyGiphy) { 25 | div("giphy-detail col-md-8") { 26 | div("embed-responsive embed-responsive-16by9") { 27 | iframe(classes = "embed-responsive-item") { 28 | attrs { 29 | src = giphy.giphyUrl 30 | title = giphy.fileName 31 | } 32 | } 33 | } 34 | div("details text-center") { 35 | a(giphy.downloadUrl, classes = "btn btn-primary") { 36 | attrs { 37 | title = giphy.fileName 38 | role = "button" 39 | target = "_blank" 40 | } 41 | +"Download from Giphy" 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | } 49 | 50 | 51 | fun RBuilder.giphyDetails(giphy: Giphy = DummyGiphy) = child(GiphyDetails::class) { 52 | attrs.giphy = giphy 53 | } 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/giphy/GiphyList.kt: -------------------------------------------------------------------------------- 1 | package giphy 2 | 3 | import react.RBuilder 4 | import react.RComponent 5 | import react.RProps 6 | import react.RState 7 | import react.dom.ul 8 | 9 | interface GiphyListProps : RProps { 10 | var giphies: Array 11 | var onSelect: (Giphy) -> Unit 12 | } 13 | 14 | class GiphyList(props: GiphyListProps) : RComponent(props) { 15 | 16 | override fun RBuilder.render() { 17 | ul("col-md-4 list-group") { 18 | props.giphies.map { giphy -> 19 | giphyListItem(giphy, props.onSelect) 20 | } 21 | } 22 | } 23 | 24 | } 25 | 26 | fun RBuilder.giphyList(giphies: Array, onSelect: (Giphy) -> Unit) = child(GiphyList::class) { 27 | attrs.giphies = giphies 28 | attrs.onSelect = onSelect 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/giphy/GiphyListItem.kt: -------------------------------------------------------------------------------- 1 | package giphy 2 | 3 | import kotlinx.html.js.onClickFunction 4 | import kotlinx.html.title 5 | import react.* 6 | import react.dom.div 7 | import react.dom.img 8 | import react.dom.li 9 | 10 | interface GiphyListItemProps : RProps { 11 | var giphy: Giphy 12 | var onSelect: (Giphy) -> Unit 13 | } 14 | 15 | class GiphyListItem(props: GiphyListItemProps) : RComponent(props) { 16 | 17 | override fun RBuilder.render() { 18 | val giphy = props.giphy 19 | 20 | li("list-group-item") { 21 | attrs.onClickFunction = { props.onSelect(giphy) } 22 | 23 | div("giphy-item media") { 24 | div("media-left") { 25 | img("media-object") { 26 | attrs { 27 | src = giphy.previewUrl 28 | alt = giphy.fileName 29 | title = giphy.fileName 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | } 38 | 39 | 40 | fun RBuilder.giphyListItem(giphy: Giphy, onSelect: (Giphy) -> Unit) = child(GiphyListItem::class) { 41 | attrs.giphy = giphy 42 | attrs.key = giphy.id 43 | attrs.onSelect = onSelect 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/giphy/giphy.css: -------------------------------------------------------------------------------- 1 | 2 | .giphy-item img { 3 | max-width: 80%; 4 | } 5 | 6 | .giphy-detail .details { 7 | margin: 10px 0 10px 0; 8 | } 9 | 10 | .list-group-item { 11 | cursor: pointer; 12 | } 13 | 14 | .list-group-item:hover { 15 | background-color: #eee; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/index/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/index/index.kt: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import app.app 4 | import kotlinext.js.require 5 | import kotlinext.js.requireAll 6 | import react.dom.render 7 | import kotlin.browser.document 8 | 9 | fun main(args: Array) { 10 | requireAll(require.context("src", true, js("/\\.css$/"))) 11 | 12 | render(document.getElementById("root")) { 13 | app() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lodash/Lodash.kt: -------------------------------------------------------------------------------- 1 | package lodash 2 | 3 | @JsModule("lodash") 4 | external val lodash: Lodash 5 | 6 | external interface Lodash { 7 | fun debounce(functionToDebounce: (K) -> V, debounceMillis: Int): (K) -> V 8 | } 9 | 10 | 11 | --------------------------------------------------------------------------------