├── .github └── FUNDING.yml ├── .gitignore ├── READMe.md ├── components ├── CodePart.js └── nav.js ├── helpers ├── index.js └── useLocalStorage.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── index.js └── style.css ├── preview.png ├── public ├── cover.png └── favicon.ico └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://sendacoin.to/devzstudio"] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | .now -------------------------------------------------------------------------------- /READMe.md: -------------------------------------------------------------------------------- 1 | # SQL to Graphql Schema Generator 2 | 3 | ⚛️ Generate GraphQL Scheme Online From SQL Query 4 | 5 | [![SQL to Graphql Schema Generator](https://github.com/Devzstudio/SQL-to-Graphql-Schema-Generator/blob/master/preview.png?raw=true 'SQL to Graphql Schema Generator')]() 6 | 7 | Check live on https://sql-to-graphql.now.sh/ 8 | 9 |

10 | Made with ❤️ by Twitter Follow 11 | Twitter Follow 12 |

13 | 14 | ## ✨ Features 15 | 16 | - Convert SQL Table To GraphQL Schema 17 | 18 | - Copy Generated Code to Clipboard 19 | 20 | - Dark Mode 21 | 22 | ## 🗒 Docs 23 | 24 | - Paste SQL Query example. 25 | 26 | ``` 27 | CREATE TABLE `users` ( 28 | `id` int(11) NOT NULL AUTO_INCREMENT, 29 | `username` varchar(225) NOT NULL, 30 | `password` varchar(225) NOT NULL, 31 | `email` varchar(225) NOT NULL, 32 | `avatar` varchar(225) NOT NULL, 33 | PRIMARY KEY (`id`), 34 | UNIQUE KEY `url` (`url`) 35 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 36 | ``` 37 | 38 | - Click on the Run Button 39 | 40 | ## 🔌 Usage 41 | 42 | - Try Online : https://sql-to-graphql.now.sh/ 43 | 44 | - Or Clone the repo 45 | 46 | ``` 47 | git clone https://github.com/Devzstudio/SQL-to-Graphql-Schema-Generator.git 48 | ``` 49 | 50 | - Install dependencies and start 51 | 52 | ``` 53 | npm install 54 | ``` 55 | 56 | ``` 57 | npm run dev 58 | ``` 59 | 60 | ## 🛣 ToDo 61 | 62 | - Add Keyboard Shortcut for Paste and Copy 63 | 64 | ## 🤝 Contributing 65 | 66 | Contributions, issues and feature requests are welcome! 😍 67 | 68 | ## Show your support 69 | 70 | Give a ⭐️ if this project helped you! 🥰 71 | -------------------------------------------------------------------------------- /components/CodePart.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { UnControlled as CodeMirror } from 'react-codemirror2'; 3 | 4 | const CodePart = ({ value, onChange, mode, readOnly = false }) => { 5 | const [editor, setEditor] = useState(); 6 | const [cursor, setCursor] = useState({ 7 | line: 0, 8 | ch: 0, 9 | }); 10 | 11 | if (editor) { 12 | setEditor(undefined); 13 | } 14 | 15 | return ( 16 |
17 | { 20 | setCursor({ 21 | line: data.line, 22 | ch: data.ch + 1, 23 | }); 24 | }} 25 | autoRefresh 26 | editorDidMount={(ed) => { 27 | setEditor(ed); 28 | }} 29 | onChange={(editor, data, value) => { 30 | if (onChange) onChange(value); 31 | }} 32 | value={value} 33 | height="100%" 34 | options={{ 35 | mode, 36 | theme: 'material', 37 | lineNumbers: true, 38 | readOnly, 39 | }} 40 | /> 41 |
42 | ); 43 | }; 44 | 45 | export default CodePart; 46 | -------------------------------------------------------------------------------- /components/nav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Sun, Moon, RotateCw } from 'react-feather'; 3 | 4 | const Nav = ({ currentMode: { value }, changeMode }) => ( 5 | 84 | ); 85 | 86 | export default Nav; 87 | -------------------------------------------------------------------------------- /helpers/index.js: -------------------------------------------------------------------------------- 1 | const ucwords = (string) => string.charAt(0).toUpperCase() + string.slice(1); 2 | 3 | const selectDatatype = (fieldLineRaw) => { 4 | const fieldLine = fieldLineRaw.toLowerCase(); 5 | 6 | if ( 7 | fieldLine.includes('varchar') || 8 | fieldLine.includes('text') || 9 | fieldLine.includes('char') || 10 | fieldLine.includes('datetime') 11 | ) 12 | return 'String'; 13 | 14 | if (fieldLine.includes('decimal') || fieldLine.includes('float')) return 'Float'; 15 | 16 | if (fieldLine.includes('tinyint') || fieldLine.includes('float')) return 'Boolean'; 17 | 18 | if ( 19 | fieldLine.includes('int') || 20 | fieldLine.includes('bigint') || 21 | fieldLine.includes('numeric') || 22 | fieldLine.includes('real') 23 | ) 24 | return 'Int'; 25 | }; 26 | 27 | const checkField = (lineRaw) => { 28 | const line = lineRaw.toLowerCase(); 29 | const excludeKeywords = ['charset', 'constraint', 'foreign key', 'engine', 'create schema']; 30 | 31 | const isBlacklisted = excludeKeywords.some((keyword) => line.includes(keyword)); 32 | if (isBlacklisted) { 33 | return false; 34 | } 35 | 36 | const dataTypes = [ 37 | 'int', 38 | 'varchar', 39 | 'char', 40 | 'numeric', 41 | 'bigint', 42 | 'real', 43 | 'tinyint', 44 | 'decimal', 45 | 'text', 46 | 'float', 47 | 'datetime', 48 | ]; 49 | 50 | const checkFields = dataTypes.map((it) => { 51 | if (line.includes(it)) return true; 52 | }); 53 | 54 | if (checkFields.includes(true)) return true; 55 | return false; 56 | }; 57 | 58 | const camelCase = (str) => { 59 | return (str.slice(0, 1).toLowerCase() + str.slice(1)) 60 | .replace(/([-_ ]){1,}/g, ' ') 61 | .split(/[-_ ]/) 62 | .reduce((cur, acc) => { 63 | return cur + acc[0].toUpperCase() + acc.substring(1); 64 | }); 65 | }; 66 | 67 | const generateTableName = (str) => { 68 | return ucwords(camelCase(str)); 69 | }; 70 | 71 | const lineIsComment = (line) => { 72 | return line.startsWith('--') || line.startsWith("/*") 73 | } 74 | 75 | export { ucwords, selectDatatype, checkField, generateTableName, lineIsComment }; 76 | -------------------------------------------------------------------------------- /helpers/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const useLocalStorage = (key, initialValue) => { 4 | // State to store our value 5 | // Pass initial state function to useState so logic is only executed once 6 | const [storedValue, setStoredValue] = useState(() => { 7 | try { 8 | // Get from local storage by key 9 | const item = window.localStorage.getItem(key); 10 | // Parse stored json or if none return initialValue 11 | return item ? JSON.parse(item) : initialValue; 12 | } catch (error) { 13 | // If error also return initialValue 14 | console.log(error); 15 | return initialValue; 16 | } 17 | }); 18 | 19 | // Return a wrapped version of useState's setter function that ... 20 | // ... persists the new value to localStorage. 21 | const setValue = (value) => { 22 | try { 23 | // Allow value to be a function so we have same API as useState 24 | const valueToStore = value instanceof Function ? value(storedValue) : value; 25 | // Save state 26 | setStoredValue(valueToStore); 27 | // Save to local storage 28 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 29 | } catch (error) { 30 | // A more advanced implementation would handle the error case 31 | console.log(error); 32 | } 33 | }; 34 | 35 | return [storedValue, setValue]; 36 | }; 37 | 38 | export default useLocalStorage; 39 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withCSS = require('@zeit/next-css'); 2 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 3 | 4 | module.exports = withCSS({ 5 | webpack(config, { dev }) { 6 | if (config.mode === 'production') { 7 | if (Array.isArray(config.optimization.minimizer)) { 8 | config.optimization.minimizer.push(new OptimizeCSSAssetsPlugin({})); 9 | } 10 | } 11 | 12 | return config; 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-graphql-schema-generator", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@zeit/next-css": "^1.0.1", 12 | "codemirror": "^5.55.0", 13 | "codemirror-graphql": "^0.12.0", 14 | "graphql": "^15.3.0", 15 | "next": "9.3.2", 16 | "optimize-css-assets-webpack-plugin": "^5.0.3", 17 | "react": "^16.10.2", 18 | "react-codemirror2": "^7.2.1", 19 | "react-copy-to-clipboard": "^5.0.1", 20 | "react-dom": "16.10.2", 21 | "react-feather": "^2.0.3", 22 | "react-hotkeys-hook": "^1.5.3", 23 | "react-notify-toast": "^0.5.1", 24 | "sql-prettier": "^0.1.3", 25 | "use-dark-mode": "^2.3.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import useDarkMode from 'use-dark-mode'; 3 | import sqlPrettier from 'sql-prettier'; 4 | import dynamic from 'next/dynamic'; 5 | 6 | import { CopyToClipboard } from 'react-copy-to-clipboard'; 7 | import { Play, Clipboard } from 'react-feather'; 8 | import { useHotkeys } from 'react-hotkeys-hook'; 9 | import Notifications, { notify } from 'react-notify-toast'; 10 | 11 | import Head from 'next/head'; 12 | // import useLocalStorage from '../helpers/useLocalStorage'; 13 | 14 | import Nav from '../components/nav'; 15 | import { generateTableName, selectDatatype, checkField, lineIsComment } from '../helpers/index'; 16 | import './style.css'; 17 | 18 | import 'codemirror/lib/codemirror.css'; 19 | import('codemirror/mode/sql/sql.js'); 20 | import('codemirror-graphql/mode'); 21 | import('codemirror-graphql/hint'); 22 | import('codemirror-graphql/lint'); 23 | import('codemirror/addon/lint/lint'); 24 | import('codemirror/addon/hint/show-hint'); 25 | 26 | const CodePart = dynamic(() => import('../components/CodePart'), { ssr: false }); 27 | 28 | const Home = () => { 29 | const [query, setQuery] = useState(); 30 | const [schema, setSchema] = useState(''); 31 | const darkMode = useDarkMode(true); 32 | 33 | useHotkeys('ctrl+v', async () => { 34 | const text = await navigator.clipboard.readText(); 35 | setQuery(text); 36 | }); 37 | 38 | useHotkeys('ctrl+space', () => { 39 | makeSchema(); 40 | }); 41 | 42 | const makeSchema = () => { 43 | const newQuery = sqlPrettier.format(query); 44 | 45 | const lines = newQuery.split(/[\r\n]+/).filter(r => r.trim().length > 0); 46 | 47 | let graphqlSchema = ''; 48 | 49 | for (var i = 0; i < lines.length; i++) { 50 | 51 | const currentLine = lines[i].trim().replaceAll('`', ''); 52 | 53 | if(lineIsComment(currentLine)) { 54 | continue; 55 | } 56 | 57 | if (currentLine.toLowerCase().includes('create table')) { 58 | const tableName = currentLine 59 | .toLowerCase() 60 | .replace('create table', '') 61 | .replace(/[^\w\s]/gi, ''); 62 | graphqlSchema += `type ${generateTableName(tableName.trim())} {\r\n`; 63 | } 64 | 65 | if (currentLine.startsWith(')')) { 66 | graphqlSchema += `}\r\n\r\n`; 67 | } 68 | 69 | if (checkField(currentLine)) { 70 | const fieldLine = currentLine; 71 | const getField = fieldLine.substr(0, fieldLine.indexOf(' ')).replace(/[^\w\s]/gi, ''); 72 | const notNull = fieldLine.includes('NOT NULL'); 73 | let type = ''; 74 | if (getField === 'id' || getField.includes('_id')) type = 'ID'; 75 | else type = selectDatatype(fieldLine.toLowerCase()); 76 | graphqlSchema += ` ${getField}: ${type}${notNull ? '!' : ''}\r\n`; 77 | } 78 | 79 | setSchema(graphqlSchema); 80 | } 81 | }; 82 | 83 | return ( 84 |
85 | 86 | SQL to GraphQL Schema Generator 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
145 | ); 146 | }; 147 | 148 | export default Home; 149 | -------------------------------------------------------------------------------- /pages/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Montserrat&display=swap'); 2 | 3 | body[class='light-mode'] { 4 | --background-color: #fff; 5 | --text-color: #000; 6 | --text-area-border: #eaeaea; 7 | --btn-color: #fff; 8 | --btn-hover: #565656; 9 | --btn-text-hover: #fbfbfb; 10 | 11 | --cm-variable: #555555; 12 | } 13 | 14 | body[class='dark-mode'] { 15 | --background-color: #0f111a; 16 | --text-color: #fff; 17 | --text-area-border: #1a1c25; 18 | --btn-color: #fff; 19 | --btn-hover: #1a1c24; 20 | --btn-text-hover: #4186f5; 21 | --cm-variable: #eeffff; 22 | } 23 | 24 | ::selection { 25 | background: #79ffe1; 26 | } 27 | 28 | ::-webkit-scrollbar-track { 29 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 30 | background-color: #191c25; 31 | } 32 | 33 | ::-webkit-scrollbar { 34 | width: 5px; 35 | background-color: #191c25; 36 | } 37 | 38 | ::-webkit-scrollbar-thumb { 39 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 40 | background-color: #555; 41 | } 42 | 43 | body { 44 | font-family: Montserrat; 45 | background: var(--background-color); 46 | color: var(--text-color); 47 | } 48 | 49 | button { 50 | border: 1px solid var(--text-area-border); 51 | background: var(--btn-hover); 52 | border-radius: 100%; 53 | padding: 1rem; 54 | cursor: pointer; 55 | margin-bottom: 20px; 56 | } 57 | 58 | button > svg { 59 | color: var(--btn-color); 60 | } 61 | 62 | button:hover { 63 | border: 1px solid var(--btn-text-hover) !important; 64 | } 65 | 66 | .logo { 67 | font-family: Montserrat; 68 | display: flex; 69 | align-items: center; 70 | } 71 | .logo svg { 72 | margin-right: 10px; 73 | } 74 | 75 | .tooltip { 76 | position: relative; 77 | background: #000; 78 | padding: 5px; 79 | color: white; 80 | border-radius: 2px; 81 | left: 15px; 82 | top: -13px; 83 | font-size: 12px; 84 | } 85 | 86 | .option .tooltip { 87 | display: none; 88 | } 89 | .option:hover { 90 | color: #f1f1f1; 91 | } 92 | .option:hover .tooltip { 93 | display: inline; 94 | } 95 | 96 | .pointer { 97 | cursor: pointer; 98 | } 99 | 100 | .play-icon { 101 | position: relative; 102 | left: 2px; 103 | color: #eaeaea; 104 | } 105 | 106 | button:hover { 107 | border: 1px solid var(--btn-hover); 108 | } 109 | 110 | button:hover > svg { 111 | color: var(--btn-text-hover); 112 | } 113 | 114 | button:focus { 115 | outline: 0; 116 | } 117 | 118 | textarea { 119 | width: 100%; 120 | resize: none; 121 | height: 400px; 122 | background-color: transparent; 123 | box-shadow: none; 124 | box-sizing: border-box; 125 | display: block; 126 | font-size: 14px; 127 | line-height: 1.7; 128 | border-radius: 0px; 129 | font-family: Montserrat; 130 | height: 100%; 131 | min-height: 100px; 132 | resize: none; 133 | width: 100%; 134 | color: var(--text-color); 135 | border-radius: 0px; 136 | border-width: initial; 137 | border-style: none; 138 | border-color: initial; 139 | border-image: initial; 140 | outline: 0px; 141 | border: 1px solid var(--text-area-border); 142 | padding: 1rem; 143 | } 144 | 145 | .flex { 146 | display: flex; 147 | } 148 | .h-90 { 149 | height: 90vh; 150 | } 151 | .flex div { 152 | flex: 1; 153 | } 154 | .btn-wrapper { 155 | display: flex; 156 | justify-content: center; 157 | align-items: center; 158 | width: 0; 159 | z-index: 999; 160 | display: flex; 161 | flex-direction: column; 162 | } 163 | 164 | .hero { 165 | width: 100%; 166 | color: #333; 167 | } 168 | .title { 169 | margin: 0; 170 | width: 100%; 171 | padding-top: 80px; 172 | line-height: 1.15; 173 | font-size: 48px; 174 | } 175 | .title, 176 | .description { 177 | text-align: center; 178 | } 179 | .row { 180 | max-width: 880px; 181 | margin: 80px auto 40px; 182 | display: flex; 183 | flex-direction: row; 184 | justify-content: space-around; 185 | } 186 | 187 | .top-border { 188 | border: 1px solid var(--text-area-border); 189 | } 190 | .right-border { 191 | border-right: 1px solid var(--text-area-border); 192 | } 193 | .border-left { 194 | border-left: 1px solid var(--text-area-border); 195 | } 196 | 197 | .spons { 198 | display: flex; 199 | align-items: center; 200 | } 201 | 202 | .spons .msg { 203 | margin-right: 10px; 204 | font-weight: 600; 205 | } 206 | .spons span { 207 | color: var(--text-color); 208 | } 209 | .spons img { 210 | margin-right: 10px; 211 | } 212 | 213 | .pl-2 { 214 | padding-left: 2rem !important; 215 | padding-right: 2rem !important; 216 | background: var(--text-area-border); 217 | border-left: 1px solid #128cf0; 218 | } 219 | 220 | .theme-switch { 221 | padding: 0 2rem !important; 222 | } 223 | 224 | /* Codemirror */ 225 | .CodeMirror { 226 | font-family: Montserrat !important; 227 | min-height: 90vh !important; 228 | max-width: calc(100vw - 50vw); 229 | line-height: 1.2em; 230 | } 231 | .cm-s-material.CodeMirror { 232 | background-color: var(--background); 233 | color: var(--cm-variable); 234 | } 235 | 236 | .cm-s-material .CodeMirror-gutters { 237 | background: var(--background); 238 | color: #546e7a; 239 | border: none; 240 | } 241 | 242 | .cm-s-material .CodeMirror-guttermarker, 243 | .cm-s-material .CodeMirror-guttermarker-subtle, 244 | .cm-s-material .CodeMirror-linenumber { 245 | color: #546e7a; 246 | } 247 | .cm-s-material .CodeMirror-lines { 248 | line-height: 24px; 249 | } 250 | 251 | .cm-s-material .CodeMirror-cursor { 252 | border-left: 1px solid #ffcc00; 253 | } 254 | 255 | .cm-s-material div.CodeMirror-selected { 256 | background: rgba(128, 203, 196, 0.2); 257 | } 258 | 259 | .cm-s-material.CodeMirror-focused div.CodeMirror-selected { 260 | background: rgba(128, 203, 196, 0.2); 261 | } 262 | 263 | .cm-s-material .CodeMirror-line::selection, 264 | .cm-s-material .CodeMirror-line > span::selection, 265 | .cm-s-material .CodeMirror-line > span > span::selection { 266 | background: rgba(128, 203, 196, 0.2); 267 | } 268 | 269 | .cm-s-material .CodeMirror-line::-moz-selection, 270 | .cm-s-material .CodeMirror-line > span::-moz-selection, 271 | .cm-s-material .CodeMirror-line > span > span::-moz-selection { 272 | background: rgba(128, 203, 196, 0.2); 273 | } 274 | 275 | .cm-s-material .CodeMirror-activeline-background { 276 | background: rgba(0, 0, 0, 0.5); 277 | } 278 | 279 | .cm-s-material .cm-keyword { 280 | color: #c792ea; 281 | } 282 | 283 | .cm-s-material .cm-operator { 284 | color: #89ddff; 285 | } 286 | 287 | .cm-s-material .cm-variable-2 { 288 | color: var(--cm-variable); 289 | } 290 | 291 | .cm-s-material .cm-variable-3, 292 | .cm-s-material .cm-type { 293 | color: #f07178; 294 | } 295 | 296 | .cm-s-material .cm-builtin { 297 | color: #ffcb6b; 298 | } 299 | 300 | .cm-s-material .cm-atom { 301 | color: #f78c6c; 302 | } 303 | 304 | .cm-s-material .cm-number { 305 | color: #ff5370; 306 | } 307 | 308 | .cm-s-material .cm-def { 309 | color: #82aaff; 310 | } 311 | 312 | .cm-s-material .cm-string { 313 | color: #c3e88d; 314 | } 315 | 316 | .cm-s-material .cm-string-2 { 317 | color: #f07178; 318 | } 319 | 320 | .cm-s-material .cm-comment { 321 | color: #546e7a; 322 | } 323 | 324 | .cm-s-material .cm-variable { 325 | color: #f07178; 326 | } 327 | 328 | .cm-s-material .cm-tag { 329 | color: #ff5370; 330 | } 331 | 332 | .cm-s-material .cm-meta { 333 | color: #ffcb6b; 334 | } 335 | 336 | .cm-s-material .cm-attribute { 337 | color: #c792ea; 338 | } 339 | 340 | .cm-s-material .cm-property { 341 | color: #c792ea; 342 | } 343 | 344 | .cm-s-material .cm-qualifier { 345 | color: #decb6b; 346 | } 347 | 348 | .cm-s-material .cm-variable-3, 349 | .cm-s-material .cm-type { 350 | color: #decb6b; 351 | } 352 | 353 | .cm-s-material .cm-error { 354 | color: rgba(255, 255, 255, 1); 355 | background-color: #ff5370; 356 | } 357 | 358 | .cm-s-material .CodeMirror-matchingbracket { 359 | text-decoration: underline; 360 | color: white !important; 361 | } 362 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devzstudio/SQL-to-Graphql-Schema-Generator/fd875da4f1029c3b2a37b9242233b2afff9bd34f/preview.png -------------------------------------------------------------------------------- /public/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devzstudio/SQL-to-Graphql-Schema-Generator/fd875da4f1029c3b2a37b9242233b2afff9bd34f/public/cover.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Devzstudio/SQL-to-Graphql-Schema-Generator/fd875da4f1029c3b2a37b9242233b2afff9bd34f/public/favicon.ico --------------------------------------------------------------------------------