├── .gitignore ├── icons ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── site.webmanifest └── about.txt ├── README.md ├── scss ├── _variables.scss ├── main.scss └── _global.scss ├── js ├── elements.js ├── utils.js └── app.js ├── css ├── main.css.map └── main.css └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | TODO 2 | local/ -------------------------------------------------------------------------------- /icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muhammadpauzi/web-links-management/HEAD/icons/favicon.ico -------------------------------------------------------------------------------- /icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muhammadpauzi/web-links-management/HEAD/icons/favicon-16x16.png -------------------------------------------------------------------------------- /icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muhammadpauzi/web-links-management/HEAD/icons/favicon-32x32.png -------------------------------------------------------------------------------- /icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muhammadpauzi/web-links-management/HEAD/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Links Management 2 | 3 | Link website [https://savelinks.netlify.app/](https://savelinks.netlify.app/). 4 | -------------------------------------------------------------------------------- /icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muhammadpauzi/web-links-management/HEAD/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muhammadpauzi/web-links-management/HEAD/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $backgroundColor: #FFF; 2 | $whiteColor: #FFF; 3 | $textColor: #2C3E50; 4 | $primaryColor: #6B4FBB; 5 | $dangerColor: #ff2f2f; 6 | $grayColor: #EAEAEA; 7 | $successColor: #09C372; 8 | 9 | $bold: 600; 10 | 11 | $small: 500px; 12 | $medium: 800px; -------------------------------------------------------------------------------- /icons/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /icons/about.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following font: 2 | 3 | - Font Title: Leckerli One 4 | - Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de), with Reserved Font Names "Leckerli" 5 | - Font Source: http://fonts.gstatic.com/s/leckerlione/v11/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf 6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL) 7 | -------------------------------------------------------------------------------- /js/elements.js: -------------------------------------------------------------------------------- 1 | export const listGroup = document.querySelector('.list-group'); 2 | export const inputSearch = document.getElementById('inputSearch'); 3 | export const btnExport = document.querySelector('.btn-export'); 4 | export const btnBackToTop = document.querySelector('.btn-back-to-top'); 5 | export const sortLinks = document.getElementById('sortLinks'); 6 | export const formAddLink = document.getElementById('formAddLink'); 7 | export const formImportLinks = document.getElementById('formImportLinks'); 8 | export const linkAlert = document.querySelector('.alert.link-alert'); 9 | export const btnCollapse = document.querySelector('.btn-collapse'); 10 | export const btnDetail = document.querySelector('.btn-detail'); 11 | export const btnCloseModal = document.querySelector('.btn-close-modal'); 12 | export const formEditLink = document.getElementById('formEditLink'); 13 | export const btnShowModal = document.querySelectorAll('.btn-show-modal'); 14 | export const btnDeleteAll = document.querySelector('.btn-delete-all'); 15 | -------------------------------------------------------------------------------- /scss/main.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'global'; 3 | 4 | .navbar{ 5 | width: 100%; 6 | padding: .500rem 0; 7 | background-color: darken($whiteColor, 1); 8 | box-shadow: 0 2px 3px rgba(0,0,0,.05); 9 | margin-bottom: 2rem; 10 | } 11 | 12 | .form-create-link{ 13 | position: sticky; 14 | height: 100vh; 15 | top: 0; 16 | } 17 | 18 | .my-name{ 19 | display: inline-block; 20 | color: $primaryColor !important; 21 | 22 | &:hover{ 23 | text-decoration: underline; 24 | } 25 | } 26 | 27 | .link-icon-github{ 28 | color: $textColor; 29 | font-size: 2rem; 30 | } 31 | 32 | .grid-layout{ 33 | display: grid; 34 | grid-template-columns: 1.2fr 2fr; 35 | gap: 20px; 36 | } 37 | 38 | .grid-features{ 39 | display: grid; 40 | grid-template-columns: 1fr 1fr 1fr .3fr; 41 | gap: 5px; 42 | } 43 | 44 | .grid-import-links{ 45 | display: grid; 46 | grid-template-columns: 2fr 1fr; 47 | align-items: center; 48 | gap: 5px; 49 | } 50 | 51 | .tab-item{ 52 | display: none; 53 | } 54 | 55 | #btnAdd, #btnEdit{ 56 | width: 100%; 57 | margin-top: 1.4rem; 58 | } 59 | 60 | .btn-back-to-top{ 61 | position: fixed; 62 | bottom: 50px; 63 | right: 50px; 64 | width: 50px; 65 | height: 50px; 66 | -webkit-tap-highlight-color: transparent; 67 | display: none; 68 | align-items: center; 69 | justify-content: center; 70 | border-radius: 50%; 71 | transition: box-shadow .3s; 72 | background-color: $primaryColor; 73 | cursor: pointer; 74 | color: white; 75 | 76 | &.show{ 77 | display: flex; 78 | } 79 | 80 | &:hover{ 81 | box-shadow: 0 5px 10px rgba($primaryColor, .5); 82 | } 83 | } 84 | 85 | .btn-visit, .btn-delete, .btn-detail, .btn-show-modal-edit, .btn-delete-all{ 86 | padding: 1.4rem !important; 87 | background-repeat: no-repeat; 88 | background-position: center; 89 | background-size: 25px 21px; 90 | } 91 | 92 | .btn-visit{ 93 | margin-right: .200rem; 94 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-link-2'%3e%3cpath d='M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3'%3e%3c/path%3e%3cline x1='8' y1='12' x2='16' y2='12'%3e%3c/line%3e%3c/svg%3e"); 95 | } 96 | 97 | .btn-visit:visited{ 98 | color: $whiteColor; 99 | } 100 | 101 | .btn-delete, .btn-delete-all{ 102 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-trash-2'%3e%3cpolyline points='3 6 5 6 21 6'%3e%3c/polyline%3e%3cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'%3e%3c/path%3e%3cline x1='10' y1='11' x2='10' y2='17'%3e%3c/line%3e%3cline x1='14' y1='11' x2='14' y2='17'%3e%3c/line%3e%3c/svg%3e"); 103 | } 104 | 105 | .btn-show-modal-edit{ 106 | margin-right: .200rem; 107 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-edit'%3e%3cpath d='M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'%3e%3c/path%3e%3cpath d='M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z'%3e%3c/path%3e%3c/svg%3e"); 108 | } 109 | 110 | // for dekstop 111 | .btn-collapse{ 112 | display: none; 113 | } 114 | 115 | .btn-detail{ 116 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-chevron-down'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); 117 | } 118 | 119 | .btn-detail.active{ 120 | transform: rotate(180deg); 121 | } 122 | 123 | @media screen and(max-width: $medium){ 124 | .container{ 125 | width: 600px; 126 | } 127 | 128 | // for mobile or tablet 129 | .btn-collapse{ 130 | display: block; 131 | } 132 | 133 | .grid-layout{ 134 | grid-template-columns: 1fr; 135 | } 136 | 137 | .form-create-link{ 138 | position: static; 139 | height: auto; 140 | } 141 | } 142 | 143 | 144 | @media screen and(max-width: $small){ 145 | .btn-visit, .btn-delete, .btn-detail, .btn-show-modal-edit{ 146 | padding: 1.1rem !important; 147 | } 148 | 149 | .btn-back-to-top{ 150 | bottom: 20px; 151 | right: 20px; 152 | } 153 | 154 | .grid-import-links{ 155 | grid-template-columns: 1fr; 156 | } 157 | 158 | .footer p{ 159 | font-size: .800rem; 160 | } 161 | } -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | import { sortLinks, formEditLink } from "./elements.js"; 2 | 3 | const generateId = () => { 4 | return '000' + Math.floor(Math.random() * 1000) + 1000 + Date.now(); 5 | } 6 | 7 | const add = body => { 8 | const data = findAll(); 9 | data.push(body); 10 | save(data); 11 | } 12 | 13 | const edit = body => { 14 | const data = findAll().filter(d => d.id !== body.id); 15 | data.push(body); 16 | save(data); 17 | } 18 | 19 | const findAll = (sort = "1") => { 20 | if (localStorage.getItem('data') == null) { 21 | localStorage.setItem('data', '[]'); 22 | return []; 23 | } else { 24 | const data = JSON.parse(localStorage.getItem('data')); 25 | data.sort((a, b) => { 26 | switch (sort) { 27 | case "0": // by asc date created 28 | return a.created_at - b.created_at; 29 | case "1": // by desc data created 30 | return b.created_at - a.created_at; 31 | case "2": // by asc title 32 | if (a.title < b.title) { 33 | return -1; 34 | } 35 | if (a.title > b.title) { 36 | return 1; 37 | } 38 | return 0; 39 | case "3": // by desc title 40 | if (a.title > b.title) { 41 | return -1; 42 | } 43 | if (a.title < b.title) { 44 | return 1; 45 | } 46 | return 0; 47 | case "4": // by asc date updated 48 | if (a.updated_at && b.updated_at) { 49 | return 1; 50 | } 51 | if (a.updated_at == null) { 52 | console.log("sampai a null", a.title) 53 | return -1; 54 | } 55 | if (b.updated_at == null) { 56 | console.log("sampai b null", a.title) 57 | return 1; 58 | } 59 | return 0; 60 | case "5": // by desc date updated 61 | if (a.updated_at && b.updated_at) { 62 | console.log("sampai a dan b tidak null") 63 | return -1; 64 | } 65 | if (a.updated_at == null) { 66 | console.log("sampai a null") 67 | return 1; 68 | } 69 | if (b.updated_at == null) { 70 | console.log("sampai b null") 71 | return -1; 72 | } 73 | return 0; 74 | default: 75 | return b.created_at - a.created_at; 76 | } 77 | }); 78 | return data; 79 | } 80 | } 81 | 82 | const deleteData = id => { 83 | const newData = findAll(sortLinks.value).filter(d => d.id !== id); 84 | save(newData); 85 | } 86 | 87 | const deleteAll = () => { 88 | save([]); 89 | } 90 | 91 | const save = data => { 92 | localStorage.setItem('data', JSON.stringify(data)); 93 | } 94 | 95 | const loadDataLinkEdit = (id) => { 96 | const data = findAll().filter(d => d.id === id)[0]; 97 | formEditLink.inputTitleEdit.value = data.title; 98 | formEditLink.inputUrlEdit.value = data.url; 99 | formEditLink.inputIdEdit.value = data.id; 100 | formEditLink.inputCreatedAtEdit.value = data.created_at; 101 | } 102 | 103 | const clearDataLinkEdit = (id) => { 104 | formEditLink.inputTitleEdit.value = ''; 105 | formEditLink.inputUrlEdit.value = ''; 106 | formEditLink.inputIdEdit.value = ''; 107 | formEditLink.inputCreatedAtEdit.value = ''; 108 | } 109 | 110 | const createListItem = data => { 111 | return `
  • 112 |
    113 |
    114 | ${data.title} 115 | ${data.url} 116 |
    117 |
    118 | 119 | 120 |
    121 |
    122 |
    123 |
    124 | created at ${new Date(data.created_at).toLocaleString()} 125 | ${data.updated_at ? 'updated at ' + new Date(data.updated_at).toLocaleString() : 'Not updated yet'} 126 |
    127 |
    128 | 129 | 130 |
    131 |
    132 |
  • `; 133 | } 134 | 135 | export { 136 | generateId, 137 | add, 138 | save, 139 | findAll, 140 | createListItem, 141 | deleteData, 142 | loadDataLinkEdit, 143 | edit, 144 | clearDataLinkEdit, 145 | deleteAll 146 | } -------------------------------------------------------------------------------- /scss/_global.scss: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: -apple-system, BlinkMacSystemFont, 'Poppins', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 6 | } 7 | 8 | body{ 9 | min-height: 100vh; 10 | background-color: $backgroundColor; 11 | color: $textColor; 12 | position: relative; 13 | } 14 | 15 | // RESET 16 | 17 | a, a:visited{ 18 | display: block; 19 | text-decoration: none; 20 | color: $textColor; 21 | } 22 | 23 | li{ 24 | list-style: none; 25 | } 26 | 27 | span{ 28 | display: block; 29 | } 30 | 31 | // UTILITIES 32 | 33 | .container{ 34 | width: 1200px; 35 | max-width: 100%; 36 | padding: 0 .500rem; 37 | margin: 0 auto; 38 | } 39 | 40 | .wrapper{ 41 | padding-top: 1rem; 42 | } 43 | 44 | .devider{ 45 | display: block; 46 | height: 1px; 47 | background-color: $grayColor; 48 | border: none; 49 | margin: .200rem 0 1rem 0; 50 | width: 20px; 51 | 52 | &.full{ 53 | width: 100%; 54 | } 55 | } 56 | 57 | .error{ 58 | color: $dangerColor; 59 | text-align: center; 60 | padding: 2rem 0; 61 | font-weight: $bold; 62 | } 63 | 64 | .logo{ 65 | color: $primaryColor !important; 66 | font-weight: $bold; 67 | font-size: 1.5em; 68 | } 69 | 70 | // MARGIN & PADDING 71 | .py-1{ 72 | padding-top: .500rem !important; 73 | padding-bottom: .500rem !important; 74 | } 75 | 76 | .px-2{ 77 | padding-right: 1rem !important; 78 | padding-left: 1rem !important; 79 | } 80 | 81 | .mr-1{ 82 | margin-right: .500rem !important; 83 | } 84 | 85 | .mb-2{ 86 | margin-bottom: 1rem !important; 87 | } 88 | 89 | .mt-2{ 90 | margin-top: 1rem !important; 91 | } 92 | 93 | // DISPLAY 94 | .d-none{ 95 | display: none !important; 96 | } 97 | 98 | // FLEX 99 | 100 | .flex{ 101 | display: flex !important; 102 | } 103 | 104 | .ai-c{ 105 | align-items: center !important; 106 | } 107 | 108 | .jc-c{ 109 | justify-content: center !important; 110 | } 111 | 112 | .jc-sb{ 113 | justify-content: space-between !important; 114 | } 115 | 116 | .flex-1{ 117 | flex: 1; 118 | } 119 | 120 | // INPUTS 121 | 122 | .input{ 123 | display: block; 124 | width: 100%; 125 | padding: .900rem .600rem; 126 | font-weight: $bold; 127 | font-size: .900em; 128 | outline: none; 129 | border: 1px solid $grayColor; 130 | background-color: #FAFAFA; 131 | border-radius: 3px; 132 | } 133 | 134 | .input-group{ 135 | display: block; 136 | margin-bottom: .800rem; 137 | } 138 | 139 | // ALERT 140 | .alert{ 141 | display: flex; 142 | align-items: center; 143 | justify-content: space-between; 144 | width: 100%; 145 | padding: 1.2rem 1rem; 146 | font-weight: $bold; 147 | border-radius: 4px; 148 | margin-bottom: 1rem; 149 | 150 | .close{ 151 | cursor: pointer; 152 | } 153 | } 154 | 155 | .alert-success{ 156 | background-color: rgba($successColor, .2); 157 | color: darken($successColor, 2); 158 | border: 1px solid darken($successColor, 2); 159 | } 160 | 161 | .alert-danger{ 162 | background-color: rgba($dangerColor, .2); 163 | color: darken($dangerColor, 2); 164 | border: 1px solid darken($dangerColor, 2); 165 | } 166 | 167 | // LABELS 168 | 169 | .label { 170 | display: block; 171 | font-weight: $bold; 172 | margin: .300rem 0; 173 | } 174 | 175 | // BUTTONS 176 | 177 | .btn{ 178 | display: block; 179 | padding: .700rem .500rem; 180 | user-select: none; 181 | -webkit-tap-highlight-color: transparent; 182 | cursor: pointer; 183 | font-weight: $bold; 184 | font-size: .900em; 185 | outline: none; 186 | appearance: none; 187 | border: none; 188 | border-radius: 3px; 189 | } 190 | 191 | .btn-primary{ 192 | background-color: $primaryColor; 193 | color: $whiteColor; 194 | transition: background-color .3s; 195 | 196 | &:hover{ 197 | background-color: darken($primaryColor, 4); 198 | } 199 | } 200 | 201 | .btn-secondary{ 202 | background-color: $grayColor; 203 | color: $textColor; 204 | transition: background-color .3s; 205 | 206 | &:hover{ 207 | background-color: darken($grayColor, 4); 208 | } 209 | } 210 | 211 | .btn-danger{ 212 | background-color: $dangerColor; 213 | color: $whiteColor; 214 | transition: background-color .3s; 215 | 216 | &:hover{ 217 | background-color: darken($dangerColor, 4); 218 | } 219 | } 220 | 221 | // LISTS 222 | 223 | .list-group{ 224 | padding: .300rem 0; 225 | 226 | .list-item{ 227 | width: 100%; 228 | border: 1px solid $grayColor; 229 | font-weight: $bold; 230 | padding: .600rem .900rem; 231 | margin: .200rem 0; 232 | border-radius: 3px; 233 | transition: background-color .3s; 234 | -webkit-tap-highlight-color: transparent; 235 | 236 | .item-title{ 237 | overflow-wrap: anywhere; 238 | margin: .300rem 0; 239 | } 240 | 241 | .item-url{ 242 | margin: .300rem 0; 243 | font-size: .800em; 244 | color: $primaryColor; 245 | } 246 | 247 | .item-created, .item-updated{ 248 | margin: .300rem 0; 249 | background-color: $grayColor; 250 | padding: .300rem .400rem; 251 | font-size: .800em; 252 | display: block; 253 | width: max-content; 254 | color: $textColor; 255 | } 256 | 257 | } 258 | } 259 | 260 | // MODAL 261 | .backdrop{ 262 | position: fixed; 263 | top: 0; 264 | left: 0; 265 | right: 0; 266 | bottom: 0; 267 | background-color: rgba(0,0,0,0.4); 268 | padding: 2rem 0 1rem 0; 269 | display: none; 270 | } 271 | 272 | .backdrop.show{ 273 | display: block; 274 | } 275 | 276 | .modal{ 277 | width: 100%; 278 | max-width: 700px; 279 | background-color: $whiteColor; 280 | padding: 2rem 1rem; 281 | border-radius: 4px; 282 | margin: auto; 283 | } 284 | 285 | .modal-header{ 286 | display: flex; 287 | align-items: center; 288 | justify-content: space-between; 289 | } 290 | 291 | // HEADINGS 292 | 293 | .title{ 294 | margin-bottom: .500rem; 295 | } 296 | 297 | @media (max-width: $small){ 298 | .title{ 299 | font-size: 1.5rem; 300 | } 301 | 302 | .title-md{ 303 | font-size: 1.3rem; 304 | } 305 | } 306 | 307 | // TEXTS 308 | .text-primary{ 309 | color: $primaryColor !important; 310 | } 311 | 312 | // FOOTERS 313 | 314 | .footer{ 315 | background-color: $whiteColor; 316 | border-top: 1px solid $grayColor; 317 | padding: 1.2em 0; 318 | text-align: center; 319 | margin-top: 5rem; 320 | 321 | p{ 322 | font-weight: $bold; 323 | 324 | span{ 325 | display: inline-block; 326 | color: $primaryColor; 327 | } 328 | } 329 | } 330 | 331 | @media screen and(max-width: $small){ 332 | .list-group{ 333 | .list-item{ 334 | padding: .200rem .500rem .400rem .500rem; 335 | 336 | .item-title{ 337 | margin: .300rem 0; 338 | font-size: .900em; 339 | } 340 | 341 | .item-url{ 342 | font-size: .700em; 343 | } 344 | 345 | .item-created, .item-updated{ 346 | font-size: .600em; 347 | } 348 | } 349 | } 350 | } -------------------------------------------------------------------------------- /css/main.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "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", 4 | "sources": [ 5 | "../scss/main.scss", 6 | "../scss/_variables.scss", 7 | "../scss/_global.scss" 8 | ], 9 | "names": [], 10 | "file": "main.css" 11 | } -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | import { generateId, add, deleteData, findAll, createListItem, save, loadDataLinkEdit, edit, clearDataLinkEdit, deleteAll } from "./utils.js"; 2 | import { btnBackToTop, formAddLink, formImportLinks, btnExport, inputSearch, linkAlert, listGroup, sortLinks, btnCollapse, btnCloseModal, formEditLink, btnDeleteAll } from "./elements.js"; 3 | 4 | const showAlert = (message, color = 'success') => { 5 | linkAlert.firstElementChild.textContent = message; 6 | linkAlert.setAttribute('class', 'alert alert-' + color); 7 | linkAlert.style.display = 'flex'; 8 | } 9 | 10 | const handleAddEvent = function (e) { 11 | e.preventDefault(); 12 | const body = { 13 | id: generateId(), 14 | title: this.inputTitle.value.trim(), 15 | url: this.inputUrl.value.trim(), 16 | created_at: Date.now(), 17 | updated_at: null, 18 | } 19 | 20 | add(body); 21 | showList(inputSearch.value, sortLinks.value); 22 | this.inputTitle.value = ''; 23 | this.inputUrl.value = ''; 24 | showAlert('Link has been added.'); 25 | } 26 | 27 | const handleEditEvent = function (e) { 28 | e.preventDefault(); 29 | const body = { 30 | id: this.inputIdEdit.value, 31 | title: this.inputTitleEdit.value.trim(), 32 | url: this.inputUrlEdit.value.trim(), 33 | created_at: parseInt(this.inputCreatedAtEdit.value), 34 | updated_at: Date.now(), 35 | } 36 | 37 | edit(body); 38 | showList(inputSearch.value, sortLinks.value); 39 | this.inputIdEdit.value = ''; 40 | this.inputTitleEdit.value = ''; 41 | this.inputUrlEdit.value = ''; 42 | showAlert('Link has been edited.'); 43 | document.querySelector('.backdrop').classList.remove('show'); 44 | } 45 | 46 | let jsonFile = null; 47 | const handleExportLinks = () => { 48 | if (confirm('Are you sure to download this file?')) { 49 | // get current links 50 | const links = findAll(); 51 | // convert links to blob with the type of json 52 | const data = new Blob([JSON.stringify(links)], { type: "application/json" }); 53 | // Avoid memory leaks 54 | if (jsonFile !== null) { 55 | URL.revokeObjectURL(jsonFile); 56 | } 57 | 58 | jsonFile = URL.createObjectURL(data); 59 | const link = document.createElement('a'); 60 | link.setAttribute('download', 'links.json'); 61 | link.href = jsonFile; 62 | document.body.appendChild(link); 63 | 64 | window.requestAnimationFrame(function () { 65 | const event = new MouseEvent('click'); 66 | link.dispatchEvent(event); 67 | document.body.removeChild(link); 68 | }); 69 | } 70 | } 71 | 72 | const handleImportLinks = function (e) { 73 | e.preventDefault(); 74 | const file = this.fileJson.files[0]; 75 | if (file.type !== "application/json") { 76 | return showAlert('Import failed, The File type is not valid.', 'danger'); 77 | } 78 | const reader = new FileReader(); 79 | reader.addEventListener('load', (e) => { 80 | const links = findAll(); 81 | let importedLinks = JSON.parse(e.target.result); 82 | let keys; 83 | if (!Array.isArray(importedLinks)) { 84 | keys = Object.keys(importedLinks); 85 | } else { 86 | keys = Object.keys(importedLinks[0]); 87 | } 88 | if (!keys.includes('id') || !keys.includes('title') || !keys.includes('created_at') || !keys.includes('url') || !keys.includes('updated_at')) { 89 | return showAlert('Import failed, The file is wrong.', 'danger'); 90 | } 91 | links.map(link => { 92 | importedLinks.map(importedLink => { 93 | if (link.id == importedLink.id) { 94 | importedLink.id = generateId(); 95 | } 96 | }) 97 | }); 98 | links.push(...importedLinks); 99 | save(links); 100 | showList(inputSearch.value, sortLinks.value); 101 | showAlert('Link has been imported.'); 102 | }); 103 | reader.readAsBinaryString(file); 104 | this.reset(); 105 | } 106 | 107 | const showList = (keyword = null, sort) => { 108 | const data = findAll(sort); 109 | let temp = ''; 110 | // Search links 111 | if (keyword) { 112 | data.map(d => { 113 | if (d.title.toLowerCase().includes(keyword.toLowerCase()) || d.url.toLowerCase().includes(keyword.toLowerCase())) { 114 | temp += createListItem(d); 115 | } 116 | }); 117 | // Message not found 118 | if (temp === '') { 119 | return listGroup.innerHTML = '

    Links not found.

    '; 120 | } 121 | } else { 122 | data.map(d => temp += createListItem(d)); 123 | } 124 | // Message not found 125 | if (!data || data.length <= 0) { 126 | return listGroup.innerHTML = '

    Links not added yet.

    '; 127 | } 128 | listGroup.innerHTML = temp; 129 | } 130 | 131 | // close alert 132 | linkAlert.lastElementChild.addEventListener('click', function () { 133 | linkAlert.style.display = 'none'; 134 | }); 135 | 136 | window.addEventListener('scroll', () => { 137 | if (window.scrollY != 0) { 138 | btnBackToTop.classList.add('show'); 139 | } else { 140 | btnBackToTop.classList.remove('show'); 141 | } 142 | }); 143 | btnExport.addEventListener('click', handleExportLinks); 144 | formAddLink.addEventListener('submit', handleAddEvent); 145 | formImportLinks.addEventListener('submit', handleImportLinks); 146 | btnBackToTop.addEventListener('click', () => { 147 | scrollTo({ 148 | behavior: 'smooth', 149 | top: 0 150 | }); 151 | }); 152 | sortLinks.addEventListener('change', (e) => { 153 | showList(inputSearch.value, e.target.value); 154 | }); 155 | inputSearch.addEventListener('keyup', (e) => { 156 | showList(e.target.value); 157 | }); 158 | document.addEventListener('click', (e) => { 159 | if (e.target.classList.contains('btn-delete')) { 160 | if (confirm("Are you sure to remove this link?")) { 161 | const id = e.target.dataset.id; 162 | deleteData(id); 163 | showList(inputSearch.value, sortLinks.value); 164 | showAlert('Link has been deleted.'); 165 | } 166 | } 167 | if (e.target.classList.contains('btn-detail')) { 168 | // rotate icon / image 169 | e.target.classList.toggle('active'); 170 | // get element list-detail 171 | const elementDetail = e.target.parentElement.parentElement.nextElementSibling; 172 | // show element 173 | elementDetail.classList.toggle('flex'); 174 | } 175 | if (e.target.classList.contains('btn-show-modal-edit')) { 176 | loadDataLinkEdit(e.target.dataset.id); 177 | const elementModal = document.getElementById(e.target.dataset.target); 178 | elementModal.classList.toggle('show'); 179 | } 180 | }); 181 | // handle collapse form 182 | btnCollapse.addEventListener('click', function () { 183 | const targetElementCollapse = document.getElementById(this.dataset.target); 184 | if (targetElementCollapse.classList.contains('d-none')) { 185 | this.textContent = "Hide Form"; 186 | } else { 187 | this.textContent = "Show Form"; 188 | } 189 | targetElementCollapse.classList.toggle('d-none'); 190 | }); 191 | // hide/close modal 192 | btnCloseModal.addEventListener('click', function () { 193 | const elementModal = document.getElementById(this.dataset.target); 194 | clearDataLinkEdit(); 195 | elementModal.classList.remove('show'); 196 | }); 197 | // handle edit 198 | formEditLink.addEventListener('submit', handleEditEvent); 199 | btnDeleteAll.addEventListener('click', () => { 200 | if (confirm('Are you sure to delete all links?')) { 201 | deleteAll(); 202 | showList(); 203 | showAlert('All Links have been deleted.'); 204 | } 205 | }); 206 | 207 | showList(); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | Web Links Management | savelinks. 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 |
    43 |
    44 | 65 | 66 | 67 |
    68 | 72 |

    Links List

    73 |
    74 | 75 |
    76 | 77 | 80 |
    81 | 82 |
    83 | 85 | 87 | 88 | 89 |
    90 | 91 |
    92 |
    93 | 97 |
    98 | 99 |
    100 |
    101 |
    102 | 103 | 111 |
    112 |
    113 |
    114 |
    115 | 116 | 117 |
      118 | 119 |
      120 |
      121 |
      122 | 123 |
      124 |
      125 | 150 |
      151 |
      152 | 153 | 154 |
      155 | 156 |
      157 | 158 | 159 | 165 | 166 | 167 | 168 | 169 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | -webkit-box-sizing: border-box; 5 | box-sizing: border-box; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Poppins', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 7 | } 8 | 9 | body { 10 | min-height: 100vh; 11 | background-color: #FFF; 12 | color: #2C3E50; 13 | position: relative; 14 | } 15 | 16 | a, a:visited { 17 | display: block; 18 | text-decoration: none; 19 | color: #2C3E50; 20 | } 21 | 22 | li { 23 | list-style: none; 24 | } 25 | 26 | span { 27 | display: block; 28 | } 29 | 30 | .container { 31 | width: 1200px; 32 | max-width: 100%; 33 | padding: 0 .500rem; 34 | margin: 0 auto; 35 | } 36 | 37 | .wrapper { 38 | padding-top: 1rem; 39 | } 40 | 41 | .devider { 42 | display: block; 43 | height: 1px; 44 | background-color: #EAEAEA; 45 | border: none; 46 | margin: .200rem 0 1rem 0; 47 | width: 20px; 48 | } 49 | 50 | .devider.full { 51 | width: 100%; 52 | } 53 | 54 | .error { 55 | color: #ff2f2f; 56 | text-align: center; 57 | padding: 2rem 0; 58 | font-weight: 600; 59 | } 60 | 61 | .logo { 62 | color: #6B4FBB !important; 63 | font-weight: 600; 64 | font-size: 1.5em; 65 | } 66 | 67 | .py-1 { 68 | padding-top: .500rem !important; 69 | padding-bottom: .500rem !important; 70 | } 71 | 72 | .px-2 { 73 | padding-right: 1rem !important; 74 | padding-left: 1rem !important; 75 | } 76 | 77 | .mr-1 { 78 | margin-right: .500rem !important; 79 | } 80 | 81 | .mb-2 { 82 | margin-bottom: 1rem !important; 83 | } 84 | 85 | .mt-2 { 86 | margin-top: 1rem !important; 87 | } 88 | 89 | .d-none { 90 | display: none !important; 91 | } 92 | 93 | .flex { 94 | display: -webkit-box !important; 95 | display: -ms-flexbox !important; 96 | display: flex !important; 97 | } 98 | 99 | .ai-c { 100 | -webkit-box-align: center !important; 101 | -ms-flex-align: center !important; 102 | align-items: center !important; 103 | } 104 | 105 | .jc-c { 106 | -webkit-box-pack: center !important; 107 | -ms-flex-pack: center !important; 108 | justify-content: center !important; 109 | } 110 | 111 | .jc-sb { 112 | -webkit-box-pack: justify !important; 113 | -ms-flex-pack: justify !important; 114 | justify-content: space-between !important; 115 | } 116 | 117 | .flex-1 { 118 | -webkit-box-flex: 1; 119 | -ms-flex: 1; 120 | flex: 1; 121 | } 122 | 123 | .input { 124 | display: block; 125 | width: 100%; 126 | padding: .900rem .600rem; 127 | font-weight: 600; 128 | font-size: .900em; 129 | outline: none; 130 | border: 1px solid #EAEAEA; 131 | background-color: #FAFAFA; 132 | border-radius: 3px; 133 | } 134 | 135 | .input-group { 136 | display: block; 137 | margin-bottom: .800rem; 138 | } 139 | 140 | .alert { 141 | display: -webkit-box; 142 | display: -ms-flexbox; 143 | display: flex; 144 | -webkit-box-align: center; 145 | -ms-flex-align: center; 146 | align-items: center; 147 | -webkit-box-pack: justify; 148 | -ms-flex-pack: justify; 149 | justify-content: space-between; 150 | width: 100%; 151 | padding: 1.2rem 1rem; 152 | font-weight: 600; 153 | border-radius: 4px; 154 | margin-bottom: 1rem; 155 | } 156 | 157 | .alert .close { 158 | cursor: pointer; 159 | } 160 | 161 | .alert-success { 162 | background-color: rgba(9, 195, 114, 0.2); 163 | color: #09b96c; 164 | border: 1px solid #09b96c; 165 | } 166 | 167 | .alert-danger { 168 | background-color: rgba(255, 47, 47, 0.2); 169 | color: #ff2525; 170 | border: 1px solid #ff2525; 171 | } 172 | 173 | .label { 174 | display: block; 175 | font-weight: 600; 176 | margin: .300rem 0; 177 | } 178 | 179 | .btn { 180 | display: block; 181 | padding: .700rem .500rem; 182 | -webkit-user-select: none; 183 | -moz-user-select: none; 184 | -ms-user-select: none; 185 | user-select: none; 186 | -webkit-tap-highlight-color: transparent; 187 | cursor: pointer; 188 | font-weight: 600; 189 | font-size: .900em; 190 | outline: none; 191 | -webkit-appearance: none; 192 | -moz-appearance: none; 193 | appearance: none; 194 | border: none; 195 | border-radius: 3px; 196 | } 197 | 198 | .btn-primary { 199 | background-color: #6B4FBB; 200 | color: #FFF; 201 | -webkit-transition: background-color .3s; 202 | transition: background-color .3s; 203 | } 204 | 205 | .btn-primary:hover { 206 | background-color: #6144b1; 207 | } 208 | 209 | .btn-secondary { 210 | background-color: #EAEAEA; 211 | color: #2C3E50; 212 | -webkit-transition: background-color .3s; 213 | transition: background-color .3s; 214 | } 215 | 216 | .btn-secondary:hover { 217 | background-color: #e0e0e0; 218 | } 219 | 220 | .btn-danger { 221 | background-color: #ff2f2f; 222 | color: #FFF; 223 | -webkit-transition: background-color .3s; 224 | transition: background-color .3s; 225 | } 226 | 227 | .btn-danger:hover { 228 | background-color: #ff1b1b; 229 | } 230 | 231 | .list-group { 232 | padding: .300rem 0; 233 | } 234 | 235 | .list-group .list-item { 236 | width: 100%; 237 | border: 1px solid #EAEAEA; 238 | font-weight: 600; 239 | padding: .600rem .900rem; 240 | margin: .200rem 0; 241 | border-radius: 3px; 242 | -webkit-transition: background-color .3s; 243 | transition: background-color .3s; 244 | -webkit-tap-highlight-color: transparent; 245 | } 246 | 247 | .list-group .list-item .item-title { 248 | overflow-wrap: anywhere; 249 | margin: .300rem 0; 250 | } 251 | 252 | .list-group .list-item .item-url { 253 | margin: .300rem 0; 254 | font-size: .800em; 255 | color: #6B4FBB; 256 | } 257 | 258 | .list-group .list-item .item-created, .list-group .list-item .item-updated { 259 | margin: .300rem 0; 260 | background-color: #EAEAEA; 261 | padding: .300rem .400rem; 262 | font-size: .800em; 263 | display: block; 264 | width: -webkit-max-content; 265 | width: -moz-max-content; 266 | width: max-content; 267 | color: #2C3E50; 268 | } 269 | 270 | .backdrop { 271 | position: fixed; 272 | top: 0; 273 | left: 0; 274 | right: 0; 275 | bottom: 0; 276 | background-color: rgba(0, 0, 0, 0.4); 277 | padding: 2rem 0 1rem 0; 278 | display: none; 279 | } 280 | 281 | .backdrop.show { 282 | display: block; 283 | } 284 | 285 | .modal { 286 | width: 100%; 287 | max-width: 700px; 288 | background-color: #FFF; 289 | padding: 2rem 1rem; 290 | border-radius: 4px; 291 | margin: auto; 292 | } 293 | 294 | .modal-header { 295 | display: -webkit-box; 296 | display: -ms-flexbox; 297 | display: flex; 298 | -webkit-box-align: center; 299 | -ms-flex-align: center; 300 | align-items: center; 301 | -webkit-box-pack: justify; 302 | -ms-flex-pack: justify; 303 | justify-content: space-between; 304 | } 305 | 306 | .title { 307 | margin-bottom: .500rem; 308 | } 309 | 310 | @media (max-width: 500px) { 311 | .title { 312 | font-size: 1.5rem; 313 | } 314 | .title-md { 315 | font-size: 1.3rem; 316 | } 317 | } 318 | 319 | .text-primary { 320 | color: #6B4FBB !important; 321 | } 322 | 323 | .footer { 324 | background-color: #FFF; 325 | border-top: 1px solid #EAEAEA; 326 | padding: 1.2em 0; 327 | text-align: center; 328 | margin-top: 5rem; 329 | } 330 | 331 | .footer p { 332 | font-weight: 600; 333 | } 334 | 335 | .footer p span { 336 | display: inline-block; 337 | color: #6B4FBB; 338 | } 339 | 340 | @media screen and (max-width: 500px) { 341 | .list-group .list-item { 342 | padding: .200rem .500rem .400rem .500rem; 343 | } 344 | .list-group .list-item .item-title { 345 | margin: .300rem 0; 346 | font-size: .900em; 347 | } 348 | .list-group .list-item .item-url { 349 | font-size: .700em; 350 | } 351 | .list-group .list-item .item-created, .list-group .list-item .item-updated { 352 | font-size: .600em; 353 | } 354 | } 355 | 356 | .navbar { 357 | width: 100%; 358 | padding: .500rem 0; 359 | background-color: #fcfcfc; 360 | -webkit-box-shadow: 0 2px 3px rgba(0, 0, 0, 0.05); 361 | box-shadow: 0 2px 3px rgba(0, 0, 0, 0.05); 362 | margin-bottom: 2rem; 363 | } 364 | 365 | .form-create-link { 366 | position: -webkit-sticky; 367 | position: sticky; 368 | height: 100vh; 369 | top: 0; 370 | } 371 | 372 | .my-name { 373 | display: inline-block; 374 | color: #6B4FBB !important; 375 | } 376 | 377 | .my-name:hover { 378 | text-decoration: underline; 379 | } 380 | 381 | .link-icon-github { 382 | color: #2C3E50; 383 | font-size: 2rem; 384 | } 385 | 386 | .grid-layout { 387 | display: -ms-grid; 388 | display: grid; 389 | -ms-grid-columns: 1.2fr 2fr; 390 | grid-template-columns: 1.2fr 2fr; 391 | gap: 20px; 392 | } 393 | 394 | .grid-features { 395 | display: -ms-grid; 396 | display: grid; 397 | -ms-grid-columns: 1fr 1fr 1fr .3fr; 398 | grid-template-columns: 1fr 1fr 1fr .3fr; 399 | gap: 5px; 400 | } 401 | 402 | .grid-import-links { 403 | display: -ms-grid; 404 | display: grid; 405 | -ms-grid-columns: 2fr 1fr; 406 | grid-template-columns: 2fr 1fr; 407 | -webkit-box-align: center; 408 | -ms-flex-align: center; 409 | align-items: center; 410 | gap: 5px; 411 | } 412 | 413 | .tab-item { 414 | display: none; 415 | } 416 | 417 | #btnAdd, #btnEdit { 418 | width: 100%; 419 | margin-top: 1.4rem; 420 | } 421 | 422 | .btn-back-to-top { 423 | position: fixed; 424 | bottom: 50px; 425 | right: 50px; 426 | width: 50px; 427 | height: 50px; 428 | -webkit-tap-highlight-color: transparent; 429 | display: none; 430 | -webkit-box-align: center; 431 | -ms-flex-align: center; 432 | align-items: center; 433 | -webkit-box-pack: center; 434 | -ms-flex-pack: center; 435 | justify-content: center; 436 | border-radius: 50%; 437 | -webkit-transition: -webkit-box-shadow .3s; 438 | transition: -webkit-box-shadow .3s; 439 | transition: box-shadow .3s; 440 | transition: box-shadow .3s, -webkit-box-shadow .3s; 441 | background-color: #6B4FBB; 442 | cursor: pointer; 443 | color: white; 444 | } 445 | 446 | .btn-back-to-top.show { 447 | display: -webkit-box; 448 | display: -ms-flexbox; 449 | display: flex; 450 | } 451 | 452 | .btn-back-to-top:hover { 453 | -webkit-box-shadow: 0 5px 10px rgba(107, 79, 187, 0.5); 454 | box-shadow: 0 5px 10px rgba(107, 79, 187, 0.5); 455 | } 456 | 457 | .btn-visit, .btn-delete, .btn-detail, .btn-show-modal-edit, .btn-delete-all { 458 | padding: 1.4rem !important; 459 | background-repeat: no-repeat; 460 | background-position: center; 461 | background-size: 25px 21px; 462 | } 463 | 464 | .btn-visit { 465 | margin-right: .200rem; 466 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-link-2'%3e%3cpath d='M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3'%3e%3c/path%3e%3cline x1='8' y1='12' x2='16' y2='12'%3e%3c/line%3e%3c/svg%3e"); 467 | } 468 | 469 | .btn-visit:visited { 470 | color: #FFF; 471 | } 472 | 473 | .btn-delete, .btn-delete-all { 474 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-trash-2'%3e%3cpolyline points='3 6 5 6 21 6'%3e%3c/polyline%3e%3cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'%3e%3c/path%3e%3cline x1='10' y1='11' x2='10' y2='17'%3e%3c/line%3e%3cline x1='14' y1='11' x2='14' y2='17'%3e%3c/line%3e%3c/svg%3e"); 475 | } 476 | 477 | .btn-show-modal-edit { 478 | margin-right: .200rem; 479 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FFF' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-edit'%3e%3cpath d='M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'%3e%3c/path%3e%3cpath d='M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z'%3e%3c/path%3e%3c/svg%3e"); 480 | } 481 | 482 | .btn-collapse { 483 | display: none; 484 | } 485 | 486 | .btn-detail { 487 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-chevron-down'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); 488 | } 489 | 490 | .btn-detail.active { 491 | -webkit-transform: rotate(180deg); 492 | transform: rotate(180deg); 493 | } 494 | 495 | @media screen and (max-width: 800px) { 496 | .container { 497 | width: 600px; 498 | } 499 | .btn-collapse { 500 | display: block; 501 | } 502 | .grid-layout { 503 | -ms-grid-columns: 1fr; 504 | grid-template-columns: 1fr; 505 | } 506 | .form-create-link { 507 | position: static; 508 | height: auto; 509 | } 510 | } 511 | 512 | @media screen and (max-width: 500px) { 513 | .btn-visit, .btn-delete, .btn-detail, .btn-show-modal-edit { 514 | padding: 1.1rem !important; 515 | } 516 | .btn-back-to-top { 517 | bottom: 20px; 518 | right: 20px; 519 | } 520 | .grid-import-links { 521 | -ms-grid-columns: 1fr; 522 | grid-template-columns: 1fr; 523 | } 524 | .footer p { 525 | font-size: .800rem; 526 | } 527 | } 528 | /*# sourceMappingURL=main.css.map */ --------------------------------------------------------------------------------