├── .editorconfig ├── .gitignore ├── README.md ├── bun.lockb ├── index.html ├── netlify.toml ├── package.json ├── postcss.config.cjs ├── public ├── kiise.png └── layered-waves.svg ├── relay.config.cjs ├── rescript.json ├── schema.graphql ├── src ├── App.js ├── App.res ├── Base64.js ├── Base64.res ├── Colophon.js ├── Colophon.res ├── Comment.js ├── Comment.res ├── CommentInput.js ├── CommentInput.res ├── CommentRow.js ├── CommentRow.res ├── CommentSection.js ├── CommentSection.res ├── DateFormat.js ├── DateFormat.res ├── EditCategories.js ├── EditCategories.res ├── ErrorBoundary.js ├── ErrorBoundary.res ├── Exc.js ├── Exc.res ├── Firebase.js ├── Firebase.res ├── FooterContainer.js ├── FooterContainer.res ├── Heroicons.js ├── Heroicons.res ├── HideUs.js ├── HideUs.res ├── Home.js ├── Home.res ├── HomeJargonListSection.js ├── HomeJargonListSection.res ├── Hooks.js ├── Hooks.res ├── Index.js ├── Index.res ├── InfiniteScroll.js ├── InfiniteScroll.res ├── Jargon.js ├── Jargon.res ├── JargonCard.js ├── JargonCard.res ├── JargonList.js ├── JargonList.res ├── JargonPost.js ├── JargonPost.res ├── JargonRandomList.js ├── JargonRandomList.res ├── LazyComponents.js ├── LazyComponents.res ├── Loader.js ├── Loader.res ├── MathJax.js ├── MathJax.res ├── MathJaxContext.js ├── MathJaxContext.res ├── MultiValueLabel.js ├── MultiValueLabel.res ├── Navbar.js ├── Navbar.res ├── NavbarContainer.js ├── NavbarContainer.res ├── NewJargon.js ├── NewJargon.res ├── NewTranslation.js ├── NewTranslation.res ├── NewlineText.js ├── NewlineText.res ├── Profile.js ├── Profile.res ├── ReactIcons.js ├── ReactIcons.res ├── ReactSocialLoginButtons.js ├── ReactSocialLoginButtons.res ├── RelatedJargons.js ├── RelatedJargons.res ├── RelayWrapper.js ├── RelayWrapper.res ├── SearchBar.js ├── SearchBar.res ├── Select.js ├── Select.res ├── SignIn.js ├── SignIn.res ├── SignInContext.js ├── SignInContext.res ├── SignInWrapper.js ├── SignInWrapper.res ├── SignOut.js ├── SignOut.res ├── Tips.js ├── Tips.res ├── TokenContext.js ├── TokenContext.res ├── Translation.js ├── Translation.res ├── Translator.js ├── Translator.res ├── Util.js ├── Util.res ├── Why.js ├── Why.res ├── Window.js ├── Window.res ├── __generated__ │ ├── ColophonQuery_graphql.js │ ├── ColophonQuery_graphql.res │ ├── CommentInputMutation_graphql.js │ ├── CommentInputMutation_graphql.res │ ├── CommentRowMutation_graphql.js │ ├── CommentRowMutation_graphql.res │ ├── CommentSection_jargon_graphql.js │ ├── CommentSection_jargon_graphql.res │ ├── EditCategoriesCategoryQuery_graphql.js │ ├── EditCategoriesCategoryQuery_graphql.res │ ├── EditCategoriesJargonQuery_graphql.js │ ├── EditCategoriesJargonQuery_graphql.res │ ├── EditCategoriesMutation_graphql.js │ ├── EditCategoriesMutation_graphql.res │ ├── HomeCategoryQuery_graphql.js │ ├── HomeCategoryQuery_graphql.res │ ├── HomeJargonListSectionQuery_graphql.js │ ├── HomeJargonListSectionQuery_graphql.res │ ├── JargonCard_jargon_graphql.js │ ├── JargonCard_jargon_graphql.res │ ├── JargonListOrderQuery_graphql.js │ ├── JargonListOrderQuery_graphql.res │ ├── JargonListOrderRefetchQuery_graphql.js │ ├── JargonListOrderRefetchQuery_graphql.res │ ├── JargonPostQuery_graphql.js │ ├── JargonPostQuery_graphql.res │ ├── JargonRandomListOrderQuery_graphql.js │ ├── JargonRandomListOrderQuery_graphql.res │ ├── NewJargonCategoryQuery_graphql.js │ ├── NewJargonCategoryQuery_graphql.res │ ├── NewJargonMutation_graphql.js │ ├── NewJargonMutation_graphql.res │ ├── NewJargonWithoutTranslationMutation_graphql.js │ ├── NewJargonWithoutTranslationMutation_graphql.res │ ├── NewTranslationJargonQuery_graphql.js │ ├── NewTranslationJargonQuery_graphql.res │ ├── NewTranslationMutation_graphql.js │ ├── NewTranslationMutation_graphql.res │ ├── ProfileDisplayNameMutation_graphql.js │ ├── ProfileDisplayNameMutation_graphql.res │ ├── RelatedJargons_jargon_graphql.js │ ├── RelatedJargons_jargon_graphql.res │ ├── RelaySchemaAssets_graphql.js │ ├── RelaySchemaAssets_graphql.res │ ├── TipsQuery_graphql.js │ ├── TipsQuery_graphql.res │ ├── Translation_jargon_graphql.js │ ├── Translation_jargon_graphql.res │ ├── WhyQuery_graphql.js │ └── WhyQuery_graphql.res ├── firebaseConfig.js ├── main.css ├── uuid.js └── uuid.res ├── tailwind.config.cjs └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .graphql_ppx_cache 2 | .env.local 3 | .bsb.lock 4 | .merlin 5 | 6 | # Created by https://www.toptal.com/developers/gitignore/api/macOS,React,ReasonML,Emacs,yarn 7 | # Edit at https://www.toptal.com/developers/gitignore?templates=macOS,React,ReasonML,Emacs,yarn 8 | 9 | ### Emacs ### 10 | # -*- mode: gitignore; -*- 11 | *~ 12 | \#*\# 13 | /.emacs.desktop 14 | /.emacs.desktop.lock 15 | *.elc 16 | auto-save-list 17 | tramp 18 | .\#* 19 | 20 | # Org-mode 21 | .org-id-locations 22 | *_archive 23 | 24 | # flymake-mode 25 | *_flymake.* 26 | 27 | # eshell files 28 | /eshell/history 29 | /eshell/lastdir 30 | 31 | # elpa packages 32 | /elpa/ 33 | 34 | # reftex files 35 | *.rel 36 | 37 | # AUCTeX auto folder 38 | /auto/ 39 | 40 | # cask packages 41 | .cask/ 42 | dist/ 43 | 44 | # Flycheck 45 | flycheck_*.el 46 | 47 | # server auth directory 48 | /server/ 49 | 50 | # projectiles files 51 | .projectile 52 | 53 | # directory configuration 54 | .dir-locals.el 55 | 56 | # network security 57 | /network-security.data 58 | 59 | 60 | ### macOS ### 61 | # General 62 | .DS_Store 63 | .AppleDouble 64 | .LSOverride 65 | 66 | # Icon must end with two 67 | Icon 68 | 69 | # Thumbnails 70 | ._* 71 | 72 | # Files that might appear in the root of a volume 73 | .DocumentRevisions-V100 74 | .fseventsd 75 | .Spotlight-V100 76 | .TemporaryItems 77 | .Trashes 78 | .VolumeIcon.icns 79 | .com.apple.timemachine.donotpresent 80 | 81 | # Directories potentially created on remote AFP share 82 | .AppleDB 83 | .AppleDesktop 84 | Network Trash Folder 85 | Temporary Items 86 | .apdisk 87 | 88 | ### macOS Patch ### 89 | # iCloud generated files 90 | *.icloud 91 | 92 | ### react ### 93 | .DS_* 94 | *.log 95 | logs 96 | **/*.backup.* 97 | **/*.back.* 98 | 99 | node_modules 100 | bower_components 101 | 102 | *.sublime* 103 | 104 | psd 105 | thumb 106 | sketch 107 | 108 | ### Reasonml ### 109 | /lib 110 | 111 | ### yarn ### 112 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 113 | 114 | .yarn/* 115 | !.yarn/releases 116 | !.yarn/patches 117 | !.yarn/plugins 118 | !.yarn/sdks 119 | !.yarn/versions 120 | 121 | # if you are NOT using Zero-installs, then: 122 | # comment the following lines 123 | !.yarn/cache 124 | 125 | # and uncomment the following lines 126 | # .pnp.* 127 | 128 | # End of https://www.toptal.com/developers/gitignore/api/macOS,React,ReasonML,Emacs,yarn 129 | n 130 | 131 | # Local Netlify folder 132 | .netlify 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # easyword.kr 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/a2dde827-b91c-4c09-a42b-30e55db69cad/deploy-status)](https://app.netlify.com/sites/easykoreanjargon/deploys) 4 | 5 | ## Installation 6 | 7 | ```sh 8 | bun install 9 | ``` 10 | 11 | ## Build 12 | 13 | - Build: `bun build:deps` 14 | - Clean: `bun clean` 15 | - Build & watch: `bun build:watch` 16 | 17 | ## Run 18 | 19 | ```sh 20 | bun start 21 | ``` 22 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zeta611/easyword/6e1414bc2cc895af2a00bb42bff1add164eec4c9/bun.lockb -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 쉬운 전문용어 9 | 14 | 15 | 16 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "https://cse-easyword.netlify.app/*" 3 | to = "https://easyword.kr/:splat" 4 | status = 301 5 | force = true 6 | 7 | [[redirects]] 8 | from = "/*" 9 | to = "/index.html" 10 | status = 200 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easyword.kr", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "start": "concurrently \"bun run build:watch\" \"bun run relay:watch\" \"bun run dev\"", 7 | "build": "rescript", 8 | "build:deps": "rescript build -with-deps", 9 | "build:watch": "rescript build -w", 10 | "relay": "rescript-relay-compiler", 11 | "relay:watch": "rescript-relay-compiler --watch", 12 | "relay:fmt": "rescript-relay-cli format-all-graphql", 13 | "clean": "rescript clean -with-deps", 14 | "dev": "bunx --bun vite", 15 | "preview": "vite preview", 16 | "release": "vite build" 17 | }, 18 | "keywords": [ 19 | "rescript" 20 | ], 21 | "author": "Jay Lee ", 22 | "license": "MIT", 23 | "packageManager": "bun@1.1.3", 24 | "browserslist": [ 25 | "last 2 versions", 26 | "not dead", 27 | "> 0.2% in KR", 28 | "iOS >= 12.5" 29 | ], 30 | "dependencies": { 31 | "@glennsl/rescript-fetch": "^0.2.1", 32 | "@heroicons/react": "^2.1.5", 33 | "@rescript/core": "^1.5.2", 34 | "@rescript/react": "^0.12.2", 35 | "better-react-mathjax": "^2.0.3", 36 | "daisyui": "^4.12.10", 37 | "firebase": "^9.23.0", 38 | "react": "18.2.0", 39 | "react-dom": "18.2.0", 40 | "react-error-boundary": "^4.0.13", 41 | "react-icons": "^5.3.0", 42 | "react-infinite-scroll-component": "^6.1.0", 43 | "react-relay": "18.2.0", 44 | "react-select": "^5.8.0", 45 | "react-social-login-buttons": "^4.1.0", 46 | "reactfire": "^4.2.3", 47 | "relay-runtime": "18.2.0", 48 | "rescript-relay": "^3.1.0", 49 | "rescript-webapi": "^0.9.1", 50 | "uuid": "^9.0.1" 51 | }, 52 | "devDependencies": { 53 | "@tailwindcss/forms": "^0.5.7", 54 | "@tailwindcss/typography": "^0.5.14", 55 | "@vitejs/plugin-react-swc": "^3.7.0", 56 | "autoprefixer": "^10.4.20", 57 | "concurrently": "^5.3.0", 58 | "lightningcss": "^1.26.0", 59 | "postcss": "^8.4.41", 60 | "postcss-preset-env": "^9.6.0", 61 | "rescript": "^11.1.3", 62 | "tailwindcss": "^3.4.10", 63 | "terser": "^5.31.6", 64 | "vite": "^5.4.0", 65 | "@vitejs/plugin-legacy": "^5.4.1" 66 | }, 67 | "trustedDependencies": [ 68 | "@swc/core", 69 | "core-js", 70 | "esbuild", 71 | "protobufjs", 72 | "rescript", 73 | "rescript-relay" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | "postcss-preset-env": {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /public/kiise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zeta611/easyword/6e1414bc2cc895af2a00bb42bff1add164eec4c9/public/kiise.png -------------------------------------------------------------------------------- /public/layered-waves.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /relay.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | src: "./src", 3 | schema: "./schema.graphql", 4 | artifactDirectory: "./src/__generated__", 5 | customScalarTypes: { 6 | timestamptz: "string", 7 | uuid: "string", 8 | seed_float: "string", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /rescript.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eko", 3 | "version": "0.0.1", 4 | "sources": { 5 | "dir": "src", 6 | "subdirs": true 7 | }, 8 | "package-specs": { 9 | "module": "esmodule", 10 | "in-source": true 11 | }, 12 | "suffix": ".js", 13 | "jsx": { "version": 4, "mode": "automatic" }, 14 | "bs-dependencies": [ 15 | "@rescript/core", 16 | "@rescript/react", 17 | "rescript-relay", 18 | "rescript-webapi", 19 | "@glennsl/rescript-fetch" 20 | ], 21 | "warnings": { 22 | "error": "+101" 23 | }, 24 | "bsc-flags": ["-open RescriptCore"], 25 | "ppx-flags": ["rescript-relay/ppx"] 26 | } 27 | -------------------------------------------------------------------------------- /src/App.res: -------------------------------------------------------------------------------- 1 | @react.component 2 | let make = () => { 3 | open Firebase 4 | 5 | let app = useFirebaseApp() 6 | let auth = app->Auth.getAuth 7 | 8 | // let () = %raw(`self.FIREBASE_APPCHECK_DEBUG_TOKEN = true`) 9 | 10 | let appCheck = initializeAppCheck( 11 | app, 12 | { 13 | provider: createReCaptchaV3Provider(appCheckToken), 14 | isTokenAutoRefreshEnabled: true, 15 | }, 16 | ) 17 | 18 | let {status, data: firestore} = useInitFirestore(async app => app->getFirestore) 19 | 20 | let mathJaxConfig = { 21 | "loader": {"load": ["[tex]/bussproofs"]}, 22 | "tex": { 23 | "packages": {"[+]": ["bussproofs"]}, 24 | "inlineMath": [["$", "$"], ["\\(", "\\)"]], 25 | "displayMath": [["$$", "$$"], ["\\[", "\\]"]], 26 | }, 27 | } 28 | 29 | let url = RescriptReactRouter.useUrl() 30 | 31 | switch status { 32 | | #loading => 33 |
34 | 35 |
36 | 37 | | #success => 38 | switch firestore { 39 | | None => React.null 40 | | Some(firestore) => 41 | 42 | 43 | 44 | 45 | 46 | 48 | 49 | }> 50 | { 51 | open LazyComponents 52 | switch url.path { 53 | | list{"login"} => 54 | | list{"logout"} => 55 | 56 | | path => 57 | 58 | 59 | {switch path { 60 | | list{} => 61 | { 63 | Console.error(error) 64 | React.null 65 | }}> 66 | 69 | 70 | }> 71 | 72 | 73 | 74 | | list{"profile"} => 75 | | list{"new-jargon"} => 76 | | list{"new-translation", jargonID} => 77 | | list{"edit-categories", jargonID} => 78 | | list{"jargon", jargonID} => 79 | { 81 |
82 | {"앗! 404"->React.string} 83 |
84 | }}> 85 | 86 | 87 | 88 |
89 | 90 | | list{"trans"} => 91 | | list{"tips"} => 92 | | list{"why"} => 93 | | list{"colophon"} => 94 | 95 | | _ => React.string("404") 96 | }} 97 |
98 |
99 | } 100 | } 101 |
102 |
103 |
104 |
105 |
106 |
107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Base64.js: -------------------------------------------------------------------------------- 1 | // Generated by ReScript, PLEASE EDIT WITH CARE 2 | 3 | import * as Js_exn from "rescript/lib/es6/js_exn.js"; 4 | import * as Core__JSON from "@rescript/core/src/Core__JSON.js"; 5 | import * as Core__Option from "@rescript/core/src/Core__Option.js"; 6 | import * as Caml_js_exceptions from "rescript/lib/es6/caml_js_exceptions.js"; 7 | 8 | function retrieveOriginalID(id) { 9 | try { 10 | var decoded = Core__JSON.Decode.array(JSON.parse(atob(id))); 11 | if (decoded !== undefined) { 12 | return decoded[3]; 13 | } else { 14 | return ; 15 | } 16 | } 17 | catch (raw_exn){ 18 | var exn = Caml_js_exceptions.internalToOCamlException(raw_exn); 19 | if (exn.RE_EXN_ID === Js_exn.$$Error) { 20 | console.error("Error decoding ID: " + id); 21 | return ; 22 | } 23 | throw exn; 24 | } 25 | } 26 | 27 | function retrieveOriginalIDString(id) { 28 | var originalID = retrieveOriginalID(id); 29 | if (originalID !== undefined) { 30 | return Core__JSON.Decode.string(originalID); 31 | } 32 | 33 | } 34 | 35 | function retrieveOriginalIDInt(id) { 36 | var originalID = retrieveOriginalID(id); 37 | if (originalID !== undefined) { 38 | return Core__Option.map(Core__JSON.Decode.$$float(originalID), (function (n) { 39 | return n | 0; 40 | })); 41 | } 42 | 43 | } 44 | 45 | export { 46 | retrieveOriginalID , 47 | retrieveOriginalIDString , 48 | retrieveOriginalIDInt , 49 | } 50 | /* No side effect */ 51 | -------------------------------------------------------------------------------- /src/Base64.res: -------------------------------------------------------------------------------- 1 | @val external encode: string => string = "btoa" 2 | @val external decode: string => string = "atob" 3 | 4 | let retrieveOriginalID = id => { 5 | try { 6 | switch id->decode->JSON.parseExn->JSON.Decode.array { 7 | | Some(decoded) => decoded[3] 8 | | None => None 9 | } 10 | } catch { 11 | | Exn.Error(_) => { 12 | Js.Console.error(`Error decoding ID: ${id}`) 13 | None 14 | } 15 | } 16 | } 17 | 18 | let retrieveOriginalIDString = id => { 19 | switch id->retrieveOriginalID { 20 | | Some(originalID) => originalID->JSON.Decode.string 21 | | None => None 22 | } 23 | } 24 | 25 | let retrieveOriginalIDInt = id => { 26 | switch id->retrieveOriginalID { 27 | | Some(originalID) => originalID->JSON.Decode.float->Option.map(n => n->Float.toInt) 28 | | None => None 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Colophon.js: -------------------------------------------------------------------------------- 1 | // Generated by ReScript, PLEASE EDIT WITH CARE 2 | 3 | import * as Core__Option from "@rescript/core/src/Core__Option.js"; 4 | import * as JsxRuntime from "react/jsx-runtime"; 5 | import * as RescriptRelay_Query from "rescript-relay/src/RescriptRelay_Query.js"; 6 | import * as ColophonQuery_graphql from "./__generated__/ColophonQuery_graphql.js"; 7 | 8 | var convertVariables = ColophonQuery_graphql.Internal.convertVariables; 9 | 10 | var convertResponse = ColophonQuery_graphql.Internal.convertResponse; 11 | 12 | var convertWrapRawResponse = ColophonQuery_graphql.Internal.convertWrapRawResponse; 13 | 14 | var use = RescriptRelay_Query.useQuery(convertVariables, ColophonQuery_graphql.node, convertResponse); 15 | 16 | var useLoader = RescriptRelay_Query.useLoader(convertVariables, ColophonQuery_graphql.node, (function (prim) { 17 | return prim; 18 | })); 19 | 20 | var usePreloaded = RescriptRelay_Query.usePreloaded(ColophonQuery_graphql.node, convertResponse, (function (prim) { 21 | return prim; 22 | })); 23 | 24 | var $$fetch = RescriptRelay_Query.$$fetch(ColophonQuery_graphql.node, convertResponse, convertVariables); 25 | 26 | var fetchPromised = RescriptRelay_Query.fetchPromised(ColophonQuery_graphql.node, convertResponse, convertVariables); 27 | 28 | var retain = RescriptRelay_Query.retain(ColophonQuery_graphql.node, convertVariables); 29 | 30 | var ColophonQuery = { 31 | Operation: undefined, 32 | Types: undefined, 33 | convertVariables: convertVariables, 34 | convertResponse: convertResponse, 35 | convertWrapRawResponse: convertWrapRawResponse, 36 | use: use, 37 | useLoader: useLoader, 38 | usePreloaded: usePreloaded, 39 | $$fetch: $$fetch, 40 | fetchPromised: fetchPromised, 41 | retain: retain 42 | }; 43 | 44 | function Colophon(props) { 45 | var match = use(undefined, undefined, undefined, undefined); 46 | var data = Core__Option.map(match.html_connection.edges[0], (function (edge) { 47 | return edge.node.data; 48 | })); 49 | if (data !== undefined) { 50 | return JsxRuntime.jsxs("article", { 51 | children: [ 52 | JsxRuntime.jsx("h1", { 53 | children: "제작기" 54 | }), 55 | JsxRuntime.jsx("div", { 56 | children: JsxRuntime.jsx("a", { 57 | children: "서울대학교 프로그래밍 연구실 이재호", 58 | href: "http://ropas.snu.ac.kr/~jhlee/" 59 | }), 60 | className: "text-right text-sm" 61 | }), 62 | JsxRuntime.jsx("div", { 63 | className: "divider" 64 | }), 65 | JsxRuntime.jsx("div", { 66 | dangerouslySetInnerHTML: { 67 | __html: data 68 | } 69 | }) 70 | ], 71 | className: "px-6 py-12 max-w-xl mx-auto md:max-w-4xl prose" 72 | }); 73 | } else { 74 | return null; 75 | } 76 | } 77 | 78 | var make = Colophon; 79 | 80 | export { 81 | ColophonQuery , 82 | make , 83 | } 84 | /* use Not a pure module */ 85 | -------------------------------------------------------------------------------- /src/Colophon.res: -------------------------------------------------------------------------------- 1 | module ColophonQuery = %relay(` 2 | query ColophonQuery { 3 | html_connection(where: { id: { _eq: 3 } }) { 4 | edges { 5 | node { 6 | data 7 | } 8 | } 9 | } 10 | } 11 | `) 12 | 13 | @react.component 14 | let make = () => { 15 | let {html_connection: {edges: htmlEdges}} = ColophonQuery.use(~variables=()) 16 | let data = htmlEdges[0]->Option.map(edge => edge.node.data) 17 | switch data { 18 | | Some(data) => 19 |
20 |

{"제작기"->React.string}

21 |
22 | 23 | {"서울대학교 프로그래밍 연구실 이재호"->React.string} 24 | 25 |
26 |
27 |
28 |
29 | | None => React.null 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Comment.js: -------------------------------------------------------------------------------- 1 | // Generated by ReScript, PLEASE EDIT WITH CARE 2 | 3 | import * as Core__List from "@rescript/core/src/Core__List.js"; 4 | import * as Core__Option from "@rescript/core/src/Core__Option.js"; 5 | 6 | function constructForest(comments) { 7 | var roots = { 8 | contents: /* [] */0 9 | }; 10 | var commentNodeTable = new Map(); 11 | comments.forEach(function (comment) { 12 | var node = { 13 | comment: comment, 14 | parent: undefined, 15 | children: /* [] */0 16 | }; 17 | commentNodeTable.set(comment.id, node); 18 | if (Core__Option.isNone(comment.parent)) { 19 | roots.contents = Core__List.add(roots.contents, node); 20 | return ; 21 | } 22 | 23 | }); 24 | commentNodeTable.forEach(function (node) { 25 | var parent = node.comment.parent; 26 | if (parent === undefined) { 27 | return ; 28 | } 29 | var parentNode = Core__Option.getExn(commentNodeTable.get(parent), undefined); 30 | parentNode.children = Core__List.add(parentNode.children, node); 31 | node.parent = parentNode; 32 | }); 33 | return roots; 34 | } 35 | 36 | function countDescendents(children) { 37 | if (!children) { 38 | return 0; 39 | } 40 | var children$1 = children.hd.children; 41 | return (1 + countDescendents(children$1) | 0) + countDescendents(children.tl) | 0; 42 | } 43 | 44 | export { 45 | constructForest , 46 | countDescendents , 47 | } 48 | /* No side effect */ 49 | -------------------------------------------------------------------------------- /src/Comment.res: -------------------------------------------------------------------------------- 1 | type t = { 2 | id: string, 3 | content: string, 4 | userDisplayName: string, 5 | userPhotoURL: option, 6 | timestamp: Date.t, 7 | parent: option, 8 | translation: option, 9 | } 10 | 11 | type rec node = { 12 | comment: t, 13 | mutable parent: option, 14 | mutable children: list, 15 | } 16 | 17 | type write = { 18 | jargonID: string, 19 | content: string, 20 | parent: string, 21 | } 22 | 23 | let constructForest = comments => { 24 | let roots: ref> = ref(list{}) 25 | let commentNodeTable = Map.make() 26 | 27 | // Store comments in the lookupComment hash map & add roots as well 28 | comments->Array.forEach(comment => { 29 | let node = {comment, parent: None, children: list{}} 30 | commentNodeTable->Map.set(comment.id, node) 31 | if comment.parent->Option.isNone { 32 | roots := roots.contents->List.add(node) 33 | } 34 | }) 35 | 36 | // Iterate through the array and link the nodes 37 | commentNodeTable->Map.forEach(({comment} as node) => { 38 | switch comment.parent { 39 | | Some(parent) => { 40 | let parentNode = commentNodeTable->Map.get(parent)->Option.getExn 41 | parentNode.children = parentNode.children->List.add(node) 42 | node.parent = Some(parentNode) 43 | } 44 | | None => () 45 | } 46 | }) 47 | 48 | roots 49 | } 50 | 51 | let rec countDescendents = children => { 52 | switch children { 53 | | list{} => 0 54 | | list{{children}, ...tl} => 1 + countDescendents(children) + countDescendents(tl) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/CommentInput.js: -------------------------------------------------------------------------------- 1 | // Generated by ReScript, PLEASE EDIT WITH CARE 2 | 3 | import * as React from "react"; 4 | import * as Base64 from "./Base64.js"; 5 | import * as SignInContext from "./SignInContext.js"; 6 | import * as JsxRuntime from "react/jsx-runtime"; 7 | import * as RescriptRelay_Mutation from "rescript-relay/src/RescriptRelay_Mutation.js"; 8 | import * as CommentInputMutation_graphql from "./__generated__/CommentInputMutation_graphql.js"; 9 | 10 | var convertVariables = CommentInputMutation_graphql.Internal.convertVariables; 11 | 12 | var convertResponse = CommentInputMutation_graphql.Internal.convertResponse; 13 | 14 | var convertWrapRawResponse = CommentInputMutation_graphql.Internal.convertWrapRawResponse; 15 | 16 | var commitMutation = RescriptRelay_Mutation.commitMutation(convertVariables, CommentInputMutation_graphql.node, convertResponse, convertWrapRawResponse); 17 | 18 | var use = RescriptRelay_Mutation.useMutation(convertVariables, CommentInputMutation_graphql.node, convertResponse, convertWrapRawResponse); 19 | 20 | var CommentMutation = { 21 | Operation: undefined, 22 | Types: undefined, 23 | convertVariables: convertVariables, 24 | convertResponse: convertResponse, 25 | convertWrapRawResponse: convertWrapRawResponse, 26 | commitMutation: commitMutation, 27 | use: use 28 | }; 29 | 30 | function CommentInput(props) { 31 | var jargonID = props.jargonID; 32 | var match = React.useContext(SignInContext.context); 33 | var user = match.user; 34 | var match$1 = React.useState(function () { 35 | return ""; 36 | }); 37 | var setContent = match$1[1]; 38 | var content = match$1[0]; 39 | var handleInputChange = function ($$event) { 40 | var value = $$event.currentTarget.value; 41 | setContent(function (param) { 42 | return value; 43 | }); 44 | }; 45 | var match$2 = use(); 46 | var mutate = match$2[0]; 47 | var handleSubmit = function ($$event) { 48 | $$event.preventDefault(); 49 | if (content.length < 3) { 50 | window.alert("댓글은 세 글자 이상이어야 해요"); 51 | return ; 52 | } 53 | var jargonID$1 = Base64.retrieveOriginalIDString(jargonID); 54 | if (user == null) { 55 | window.alert("로그인해야 합니다"); 56 | } else if (jargonID$1 !== undefined) { 57 | mutate({ 58 | authorID: user.uid, 59 | content: content, 60 | jargonID: jargonID$1, 61 | now: new Date().toISOString() 62 | }, undefined, undefined, undefined, (function (_response, _errors) { 63 | ((window.location.reload())); 64 | }), (function (error) { 65 | console.error(error); 66 | }), undefined); 67 | } else { 68 | window.alert("현재 댓글을 달 수 없어요"); 69 | } 70 | }; 71 | return JsxRuntime.jsx("form", { 72 | children: JsxRuntime.jsxs("div", { 73 | children: [ 74 | JsxRuntime.jsx("textarea", { 75 | className: "textarea textarea-ghost textarea-sm focus:outline-0 focus:border-transparent place-self-stretch", 76 | id: "comment", 77 | name: "comment", 78 | placeholder: "여러분의 생각은 어떠신가요?", 79 | value: content, 80 | onChange: handleInputChange 81 | }), 82 | JsxRuntime.jsx("input", { 83 | className: "btn btn-neutral btn-sm ml-1 mb-1 disabled:loading", 84 | disabled: match$2[1], 85 | type: "submit", 86 | value: "댓글" 87 | }) 88 | ], 89 | className: "rounded-lg border-2 border-zinc-300 focus-within:border-zinc-400 bg-white gap-1 grid grid-cols-1 place-items-start" 90 | }), 91 | onSubmit: handleSubmit 92 | }); 93 | } 94 | 95 | var make = CommentInput; 96 | 97 | export { 98 | CommentMutation , 99 | make , 100 | } 101 | /* commitMutation Not a pure module */ 102 | -------------------------------------------------------------------------------- /src/CommentInput.res: -------------------------------------------------------------------------------- 1 | module CommentMutation = %relay(` 2 | mutation CommentInputMutation( 3 | $authorID: String! 4 | $content: String! 5 | $jargonID: uuid! 6 | $now: timestamptz! 7 | ) { 8 | insert_comment_one( 9 | object: { author_id: $authorID, content: $content, jargon_id: $jargonID } 10 | ) { 11 | id 12 | } 13 | update_jargon_by_pk( 14 | pk_columns: { id: $jargonID } 15 | _set: { updated_at: $now } 16 | ) { 17 | id 18 | } 19 | } 20 | `) 21 | 22 | @react.component 23 | let make = (~jargonID) => { 24 | let {user} = React.useContext(SignInContext.context) 25 | 26 | // For handling the comment textarea 27 | let (content, setContent) = React.useState(() => "") 28 | let handleInputChange = event => { 29 | let value = ReactEvent.Form.currentTarget(event)["value"] 30 | setContent(_ => value) 31 | } 32 | 33 | let (mutate, isMutating) = CommentMutation.use() 34 | 35 | let handleSubmit = event => { 36 | // Prevent a page refresh, we are already listening for updates 37 | ReactEvent.Form.preventDefault(event) 38 | 39 | if content->String.length < 3 { 40 | Window.alert("댓글은 세 글자 이상이어야 해요") 41 | } else { 42 | let jargonID = jargonID->Base64.retrieveOriginalIDString 43 | switch (user->Nullable.toOption, jargonID) { 44 | | (Some(user), Some(jargonID)) => 45 | mutate( 46 | ~variables={ 47 | authorID: user.uid, 48 | content, 49 | jargonID, 50 | now: Date.make()->Date.toISOString, 51 | }, 52 | ~onError=error => { 53 | Console.error(error) 54 | }, 55 | ~onCompleted=(_response, _errors) => { 56 | // TODO: Make relay understand the update 57 | %raw(`window.location.reload()`) 58 | }, 59 | )->ignore 60 | | (Some(_), _) => Window.alert("현재 댓글을 달 수 없어요") 61 | | (None, _) => Window.alert("로그인해야 합니다") 62 | } 63 | } 64 | } 65 | 66 |
67 |
69 |