├── .firebaserc ├── .gitignore ├── .npmrc ├── .vscode └── extensions.json ├── README.md ├── app.vue ├── assets ├── github-logo.svg └── styles.css ├── components └── NavBar.vue ├── composables ├── domain-check.ts ├── emojis.ts └── relative-time.ts ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── middleware └── authenticated.ts ├── nuxt.config.ts ├── package.json ├── pages ├── index.vue └── login.vue ├── pnpm-lock.yaml └── tsconfig.json /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "emoji-panel-test-1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | .yalc 10 | yalc.lock 11 | .firebase 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "chflick.firecode" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # yarn 11 | yarn install 12 | 13 | # npm 14 | npm install 15 | 16 | # pnpm 17 | pnpm install --shamefully-hoist 18 | ``` 19 | 20 | ## Development Server 21 | 22 | Start the development server on http://localhost:3000 23 | 24 | ```bash 25 | npm run dev 26 | ``` 27 | 28 | ## Production 29 | 30 | Build the application for production: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | Locally preview production build: 37 | 38 | ```bash 39 | npm run preview 40 | ``` 41 | 42 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 43 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 46 | -------------------------------------------------------------------------------- /assets/github-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /assets/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --links: #00dc82; 3 | --code: currentColor; 4 | 5 | --ease-bezier: cubic-bezier(0, 0.26, 0.55, 1.18); 6 | } 7 | 8 | a { 9 | font-weight: 500; 10 | text-decoration: inherit; 11 | } 12 | a:hover { 13 | color: #4de7a8; 14 | text-decoration: inherit; 15 | } 16 | 17 | h1 { 18 | font-size: 3.2em; 19 | line-height: 1.1; 20 | } 21 | /* force emoji in specific places where the variation should always be used */ 22 | .emoji { 23 | font-family: Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, 24 | Android Emoji, EmojiSymbols, EmojiOne Mozilla, Twemoji Mozilla, 25 | Segoe UI Symbol, Noto Color Emoji Compat, emoji, noto-emojipedia-fallback; 26 | } 27 | 28 | .message-box { 29 | --border-radius: 8px; 30 | 31 | background-color: var(--background-alt); 32 | padding: 0.2em 1em; 33 | padding-left: calc(var(--border-radius) + 0.75em); 34 | margin: var(--border-radius) 0; 35 | border-radius: var(--border-radius); 36 | 37 | position: relative; 38 | } 39 | 40 | .message-box p { 41 | margin: 0; 42 | } 43 | 44 | .message-box::after { 45 | content: ''; 46 | position: absolute; 47 | z-index: 2; 48 | left: 0; 49 | top: 0; 50 | background-color: azure; 51 | width: var(--border-radius); 52 | height: 100%; 53 | border-radius: var(--border-radius) 0 0 var(--border-radius); 54 | } 55 | 56 | .mini-avatar, 57 | .nav-avatar { 58 | border-radius: 100%; 59 | width: 2em; 60 | height: 2em; 61 | max-width: 100%; 62 | height: auto; 63 | vertical-align: middle; 64 | margin-right: 0.3em; 65 | } 66 | 67 | .nav-avatar { 68 | width: 1.5em; 69 | height: 1.5em; 70 | margin: -0.5em 0.1em -0.5em 0; 71 | /* margin-right: 0.1em; */ 72 | } 73 | 74 | .logo { 75 | height: 1em; 76 | padding: 0 0.15em; 77 | } 78 | -------------------------------------------------------------------------------- /components/NavBar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 38 | 39 | 83 | -------------------------------------------------------------------------------- /composables/domain-check.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Ensures the domain is firebaseapp.com to avoid 3 | */ 4 | export function useDomainCheck() { 5 | onMounted(() => { 6 | if (process.env.NODE_ENV === 'production') { 7 | if (location.hostname.endsWith('.web.app')) { 8 | location.replace(location.href.replace('.web.app', '.firebaseapp.com')) 9 | } 10 | } 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /composables/emojis.ts: -------------------------------------------------------------------------------- 1 | export const emojiList = [ 2 | '😄', 3 | '😃', 4 | '😀', 5 | '😊', 6 | '☺️', 7 | '😉', 8 | '😍', 9 | '😘', 10 | '😚', 11 | '😗', 12 | '😙', 13 | '😜', 14 | '😝', 15 | '😛', 16 | '😳', 17 | '😁', 18 | '😔', 19 | '😌', 20 | '😒', 21 | '😞', 22 | '😣', 23 | '😢', 24 | '😂', 25 | '😭', 26 | '😪', 27 | '😥', 28 | '😰', 29 | '😅', 30 | '😓', 31 | '😩', 32 | '😫', 33 | '😨', 34 | '😱', 35 | '😠', 36 | '😡', 37 | '😤', 38 | '😖', 39 | '😆', 40 | '😋', 41 | '😷', 42 | '😎', 43 | '😴', 44 | '😵', 45 | '😲', 46 | '😟', 47 | '😦', 48 | '😧', 49 | '😈', 50 | '👿', 51 | '😮', 52 | '😬', 53 | '😐', 54 | '😕', 55 | '😯', 56 | '😶', 57 | '😇', 58 | '😏', 59 | '😑', 60 | '👲', 61 | '👳', 62 | '👮', 63 | '👷', 64 | '💂', 65 | '👶', 66 | '👦', 67 | '👧', 68 | '👨', 69 | '👩', 70 | '👴', 71 | '👵', 72 | '👱', 73 | '👼', 74 | '👸', 75 | '😺', 76 | '😸', 77 | '😻', 78 | '😽', 79 | '😼', 80 | '🙀', 81 | '😿', 82 | '😹', 83 | '😾', 84 | '👹', 85 | '👺', 86 | '🙈', 87 | '🙉', 88 | '🙊', 89 | '💀', 90 | '👽', 91 | '💩', 92 | '🔥', 93 | '✨', 94 | '🌟', 95 | '💫', 96 | '💥', 97 | '💢', 98 | '💦', 99 | '💧', 100 | '💤', 101 | '💨', 102 | '👂', 103 | '👀', 104 | '👃', 105 | '👅', 106 | '👄', 107 | '👍', 108 | '👎', 109 | '👌', 110 | '👊', 111 | '✊', 112 | '✌', 113 | '👋', 114 | '✋', 115 | '👐', 116 | '👆', 117 | '👇', 118 | '👉', 119 | '👈', 120 | '🙌', 121 | '🙏', 122 | '☝', 123 | '👏', 124 | '💪', 125 | '🚶', 126 | '🏃', 127 | '💃', 128 | '👫', 129 | '👪', 130 | '👬', 131 | '👭', 132 | '💏', 133 | '💑', 134 | '👯', 135 | '🙆', 136 | '🙅', 137 | '💁', 138 | '🙋', 139 | '💆', 140 | '💇', 141 | '💅', 142 | '👰', 143 | '🙎', 144 | '🙍', 145 | '🙇', 146 | '🎩', 147 | '👑', 148 | '👒', 149 | '👟', 150 | '👞', 151 | '👡', 152 | '👠', 153 | '👢', 154 | '👕', 155 | '👔', 156 | '👚', 157 | '👗', 158 | '🎽', 159 | '👖', 160 | '👘', 161 | '👙', 162 | '💼', 163 | '👜', 164 | '👝', 165 | '👛', 166 | '👓', 167 | '🎀', 168 | '🌂', 169 | '💄', 170 | '💛', 171 | '💙', 172 | '💜', 173 | '💚', 174 | '❤', 175 | '💔', 176 | '💗', 177 | '💓', 178 | '💕', 179 | '💖', 180 | '💞', 181 | '💘', 182 | '💌', 183 | '💋', 184 | '💍', 185 | '💎', 186 | '👤', 187 | '👥', 188 | '💬', 189 | '👣', 190 | '💭', 191 | '🐶', 192 | '🐺', 193 | '🐱', 194 | '🐭', 195 | '🐹', 196 | '🐰', 197 | '🐸', 198 | '🐯', 199 | '🐨', 200 | '🐻', 201 | '🐷', 202 | '🐽', 203 | '🐮', 204 | '🐗', 205 | '🐵', 206 | '🐒', 207 | '🐴', 208 | '🐑', 209 | '🐘', 210 | '🐼', 211 | '🐧', 212 | '🐦', 213 | '🐤', 214 | '🐥', 215 | '🐣', 216 | '🐔', 217 | '🐍', 218 | '🐢', 219 | '🐛', 220 | '🐝', 221 | '🐜', 222 | '🐞', 223 | '🐌', 224 | '🐙', 225 | '🐚', 226 | '🐠', 227 | '🐟', 228 | '🐬', 229 | '🐳', 230 | '🐋', 231 | '🐄', 232 | '🐏', 233 | '🐀', 234 | '🐃', 235 | '🐅', 236 | '🐇', 237 | '🐉', 238 | '🐎', 239 | '🐐', 240 | '🐓', 241 | '🐕', 242 | '🐖', 243 | '🐁', 244 | '🐂', 245 | '🐲', 246 | '🐡', 247 | '🐊', 248 | '🐫', 249 | '🐪', 250 | '🐆', 251 | '🐈', 252 | '🐩', 253 | '🐾', 254 | '💐', 255 | '🌸', 256 | '🌷', 257 | '🍀', 258 | '🌹', 259 | '🌻', 260 | '🌺', 261 | '🍁', 262 | '🍃', 263 | '🍂', 264 | '🌿', 265 | '🌾', 266 | '🍄', 267 | '🌵', 268 | '🌴', 269 | '🌲', 270 | '🌳', 271 | '🌰', 272 | '🌱', 273 | '🌼', 274 | '🌐', 275 | '🌞', 276 | '🌝', 277 | '🌚', 278 | '🌑', 279 | '🌒', 280 | '🌓', 281 | '🌔', 282 | '🌕', 283 | '🌖', 284 | '🌗', 285 | '🌘', 286 | '🌜', 287 | '🌛', 288 | '🌙', 289 | '🌍', 290 | '🌎', 291 | '🌏', 292 | '🌋', 293 | '🌌', 294 | '🌠', 295 | '⭐', 296 | '☀', 297 | '⛅', 298 | '☁', 299 | '⚡', 300 | '☔', 301 | '❄', 302 | '⛄', 303 | '🌀', 304 | '🌁', 305 | '🌈', 306 | '🌊', 307 | '🎍', 308 | '💝', 309 | '🎎', 310 | '🎒', 311 | '🎓', 312 | '🎏', 313 | '🎆', 314 | '🎇', 315 | '🎐', 316 | '🎑', 317 | '🎃', 318 | '👻', 319 | '🎅', 320 | '🎄', 321 | '🎁', 322 | '🎋', 323 | '🎉', 324 | '🎊', 325 | '🎈', 326 | '🎌', 327 | '🔮', 328 | '🎥', 329 | '📷', 330 | '📹', 331 | '📼', 332 | '💿', 333 | '📀', 334 | '💽', 335 | '💾', 336 | '💻', 337 | '📱', 338 | '☎', 339 | '📞', 340 | '📟', 341 | '📠', 342 | '📡', 343 | '📺', 344 | '📻', 345 | '🔊', 346 | '🔉', 347 | '🔈', 348 | '🔇', 349 | '🔔', 350 | '🔕', 351 | '📢', 352 | '📣', 353 | '⏳', 354 | '⌛', 355 | '⏰', 356 | '⌚', 357 | '🔓', 358 | '🔒', 359 | '🔏', 360 | '🔐', 361 | '🔑', 362 | '🔎', 363 | '💡', 364 | '🔦', 365 | '🔆', 366 | '🔅', 367 | '🔌', 368 | '🔋', 369 | '🔍', 370 | '🛁', 371 | '🛀', 372 | '🚿', 373 | '🚽', 374 | '🔧', 375 | '🔩', 376 | '🔨', 377 | '🚪', 378 | '🚬', 379 | '💣', 380 | '🔫', 381 | '🔪', 382 | '💊', 383 | '💉', 384 | '💰', 385 | '💴', 386 | '💵', 387 | '💷', 388 | '💶', 389 | '💳', 390 | '💸', 391 | '📲', 392 | '📧', 393 | '📥', 394 | '📤', 395 | '✉', 396 | '📩', 397 | '📨', 398 | '📯', 399 | '📫', 400 | '📪', 401 | '📬', 402 | '📭', 403 | '📮', 404 | '📦', 405 | '📝', 406 | '📄', 407 | '📃', 408 | '📑', 409 | '📊', 410 | '📈', 411 | '📉', 412 | '📜', 413 | '📋', 414 | '📅', 415 | '📆', 416 | '📇', 417 | '📁', 418 | '📂', 419 | '✂', 420 | '📌', 421 | '📎', 422 | '✒', 423 | '✏', 424 | '📏', 425 | '📐', 426 | '📕', 427 | '📗', 428 | '📘', 429 | '📙', 430 | '📓', 431 | '📔', 432 | '📒', 433 | '📚', 434 | '📖', 435 | '🔖', 436 | '📛', 437 | '🔬', 438 | '🔭', 439 | '📰', 440 | '🎨', 441 | '🎬', 442 | '🎤', 443 | '🎧', 444 | '🎼', 445 | '🎵', 446 | '🎶', 447 | '🎹', 448 | '🎻', 449 | '🎺', 450 | '🎷', 451 | '🎸', 452 | '👾', 453 | '🎮', 454 | '🃏', 455 | '🎴', 456 | '🀄', 457 | '🎲', 458 | '🎯', 459 | '🏈', 460 | '🏀', 461 | '⚽', 462 | '⚾', 463 | '🎾', 464 | '🎱', 465 | '🏉', 466 | '🎳', 467 | '⛳', 468 | '🚵', 469 | '🚴', 470 | '🏁', 471 | '🏇', 472 | '🏆', 473 | '🎿', 474 | '🏂', 475 | '🏊', 476 | '🏄', 477 | '🎣', 478 | '☕', 479 | '🍵', 480 | '🍶', 481 | '🍼', 482 | '🍺', 483 | '🍻', 484 | '🍸', 485 | '🍹', 486 | '🍷', 487 | '🍴', 488 | '🍕', 489 | '🍔', 490 | '🍟', 491 | '🍗', 492 | '🍖', 493 | '🍝', 494 | '🍛', 495 | '🍤', 496 | '🍱', 497 | '🍣', 498 | '🍥', 499 | '🍙', 500 | '🍘', 501 | '🍚', 502 | '🍜', 503 | '🍲', 504 | '🍢', 505 | '🍡', 506 | '🍳', 507 | '🍞', 508 | '🍩', 509 | '🍮', 510 | '🍦', 511 | '🍨', 512 | '🍧', 513 | '🎂', 514 | '🍰', 515 | '🍪', 516 | '🍫', 517 | '🍬', 518 | '🍭', 519 | '🍯', 520 | '🍎', 521 | '🍏', 522 | '🍊', 523 | '🍋', 524 | '🍒', 525 | '🍇', 526 | '🍉', 527 | '🍓', 528 | '🍑', 529 | '🍈', 530 | '🍌', 531 | '🍐', 532 | '🍍', 533 | '🍠', 534 | '🍆', 535 | '🍅', 536 | '🌽', 537 | '🏠', 538 | '🏡', 539 | '🏫', 540 | '🏢', 541 | '🏣', 542 | '🏥', 543 | '🏦', 544 | '🏪', 545 | '🏩', 546 | '🏨', 547 | '💒', 548 | '⛪', 549 | '🏬', 550 | '🏤', 551 | '🌇', 552 | '🌆', 553 | '🏯', 554 | '🏰', 555 | '⛺', 556 | '🏭', 557 | '🗼', 558 | '🗾', 559 | '🗻', 560 | '🌄', 561 | '🌅', 562 | '🌃', 563 | '🗽', 564 | '🌉', 565 | '🎠', 566 | '🎡', 567 | '⛲', 568 | '🎢', 569 | '🚢', 570 | '⛵', 571 | '🚤', 572 | '🚣', 573 | '⚓', 574 | '🚀', 575 | '✈', 576 | '💺', 577 | '🚁', 578 | '🚂', 579 | '🚊', 580 | '🚉', 581 | '🚞', 582 | '🚆', 583 | '🚄', 584 | '🚅', 585 | '🚈', 586 | '🚇', 587 | '🚝', 588 | '🚋', 589 | '🚃', 590 | '🚎', 591 | '🚌', 592 | '🚍', 593 | '🚙', 594 | '🚘', 595 | '🚗', 596 | '🚕', 597 | '🚖', 598 | '🚛', 599 | '🚚', 600 | '🚨', 601 | '🚓', 602 | '🚔', 603 | '🚒', 604 | '🚑', 605 | '🚐', 606 | '🚲', 607 | '🚡', 608 | '🚟', 609 | '🚠', 610 | '🚜', 611 | '💈', 612 | '🚏', 613 | '🎫', 614 | '🚦', 615 | '🚥', 616 | '⚠', 617 | '🚧', 618 | '🔰', 619 | '⛽', 620 | '🏮', 621 | '🎰', 622 | '♨', 623 | '🗿', 624 | '🎪', 625 | '🎭', 626 | '📍', 627 | '🚩', 628 | '⬆', 629 | '⬇', 630 | '⬅', 631 | '➡', 632 | '🔠', 633 | '🔡', 634 | '🔤', 635 | '↗', 636 | '↖', 637 | '↘', 638 | '↙', 639 | '↔', 640 | '↕', 641 | '🔄', 642 | '◀', 643 | '▶', 644 | '🔼', 645 | '🔽', 646 | '↩', 647 | '↪', 648 | 'ℹ', 649 | '⏪', 650 | '⏩', 651 | '⏫', 652 | '⏬', 653 | '⤵', 654 | '⤴', 655 | '🆗', 656 | '🔀', 657 | '🔁', 658 | '🔂', 659 | '🆕', 660 | '🆙', 661 | '🆒', 662 | '🆓', 663 | '🆖', 664 | '📶', 665 | '🎦', 666 | '🈁', 667 | '🈯', 668 | '🈳', 669 | '🈵', 670 | '🈴', 671 | '🈲', 672 | '🉐', 673 | '🈹', 674 | '🈺', 675 | '🈶', 676 | '🈚', 677 | '🚻', 678 | '🚹', 679 | '🚺', 680 | '🚼', 681 | '🚾', 682 | '🚰', 683 | '🚮', 684 | '🅿', 685 | '♿', 686 | '🚭', 687 | '🈷', 688 | '🈸', 689 | '🈂', 690 | 'Ⓜ', 691 | '🛂', 692 | '🛄', 693 | '🛅', 694 | '🛃', 695 | '🉑', 696 | '㊙', 697 | '㊗', 698 | '🆑', 699 | '🆘', 700 | '🆔', 701 | '🚫', 702 | '🔞', 703 | '📵', 704 | '🚯', 705 | '🚱', 706 | '🚳', 707 | '🚷', 708 | '🚸', 709 | '⛔', 710 | '✳', 711 | '❇', 712 | '❎', 713 | '✅', 714 | '✴', 715 | '💟', 716 | '🆚', 717 | '📳', 718 | '📴', 719 | '🅰', 720 | '🅱', 721 | '🆎', 722 | '🅾', 723 | '💠', 724 | '➿', 725 | '♻', 726 | '♈', 727 | '♉', 728 | '♊', 729 | '♋', 730 | '♌', 731 | '♍', 732 | '♎', 733 | '♏', 734 | '♐', 735 | '♑', 736 | '♒', 737 | '♓', 738 | '⛎', 739 | '🔯', 740 | '🏧', 741 | '💹', 742 | '💲', 743 | '💱', 744 | '©', 745 | '®', 746 | '™', 747 | '〽', 748 | '〰', 749 | '🔝', 750 | '🔚', 751 | '🔙', 752 | '🔛', 753 | '🔜', 754 | '❌', 755 | '⭕', 756 | '❗', 757 | '❓', 758 | '❕', 759 | '❔', 760 | '🔃', 761 | '🕛', 762 | '🕧', 763 | '🕐', 764 | '🕜', 765 | '🕑', 766 | '🕝', 767 | '🕒', 768 | '🕞', 769 | '🕓', 770 | '🕟', 771 | '🕔', 772 | '🕠', 773 | '🕕', 774 | '🕖', 775 | '🕗', 776 | '🕘', 777 | '🕙', 778 | '🕚', 779 | '🕡', 780 | '🕢', 781 | '🕣', 782 | '🕤', 783 | '🕥', 784 | '🕦', 785 | '✖', 786 | '➕', 787 | '➖', 788 | '➗', 789 | '♠', 790 | '♦', 791 | '♥', 792 | '♣', 793 | '💮', 794 | '💯', 795 | '✔', 796 | '☑', 797 | '🔘', 798 | '🔗', 799 | '➰', 800 | '🔱', 801 | '🔲', 802 | '🔳', 803 | '◼', 804 | '◻', 805 | '◾', 806 | '◽', 807 | '▪', 808 | '▫', 809 | '🔺', 810 | '⬜', 811 | '⬛', 812 | '⚫', 813 | '⚪', 814 | '🔴', 815 | '🔵', 816 | '🔻', 817 | '🔶', 818 | '🔷', 819 | '🔸', 820 | '🔹', 821 | ] 822 | 823 | export function getRandomEmoji() { 824 | return emojiList[Math.floor(Math.random() * emojiList.length)] 825 | } 826 | 827 | export function useRandomEmoji() { 828 | const emoji = useState('randomEmoji', () => getRandomEmoji()) 829 | 830 | function randomize() { 831 | emoji.value = getRandomEmoji() 832 | } 833 | 834 | return { emoji, randomize } 835 | } 836 | -------------------------------------------------------------------------------- /composables/relative-time.ts: -------------------------------------------------------------------------------- 1 | import { MaybeRef, useNow } from '@vueuse/core' 2 | import { Timestamp } from 'firebase/firestore' 3 | 4 | export function useRelativeTime(when: MaybeRef) { 5 | const now = useNow() 6 | 7 | return computed(() => 8 | unref(when) == null 9 | ? 'never' 10 | : relativeSeconds(now.value.getTime(), unref(when)!.seconds * 1000) 11 | ) 12 | } 13 | 14 | // time formatting 15 | const rtf = new Intl.RelativeTimeFormat('en', { style: 'long' }) 16 | function relativeSeconds(now: number, time: number) { 17 | const sinceDate = Math.min( 18 | // only allow negative values 19 | -0, 20 | Math.floor((time - now) / 1000) 21 | ) 22 | return rtf.format(sinceDate, 'seconds') 23 | } 24 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "hosting": { 7 | "public": ".output/public", 8 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | 5 | match /panelEmojis/{userId} { 6 | allow read; 7 | 8 | function isOwner() { 9 | return request.auth.uid == userId; 10 | } 11 | 12 | function hasValidData() { 13 | return ['createdAt', 'content', 'pos', 'revision'] 14 | .toSet() 15 | .difference(request.resource.data.keys().toSet()) 16 | .size() == 0 17 | && request.resource.data.createdAt == request.time 18 | && request.resource.data.pos is int 19 | && request.resource.data.content is string 20 | && request.resource.data.content.size() <= 2; 21 | } 22 | 23 | function hasValidUserInfo() { 24 | return request.resource.data.photoURL is string 25 | && request.resource.data.displayName is string; 26 | } 27 | 28 | allow create: if isOwner() 29 | && hasValidData() 30 | && hasValidUserInfo() 31 | && request.resource.data.revision == 1 32 | ; 33 | 34 | allow update: if isOwner() 35 | && hasValidData() 36 | // only allow one write every 5 seconds 37 | && request.time > resource.data.createdAt + duration.value(5, 's') 38 | // only allow increments 39 | && request.resource.data.revision == resource.data.revision + 1 40 | ; 41 | 42 | allow delete: if isOwner(); 43 | } 44 | 45 | // disable all other access 46 | match /{document=**} { 47 | allow read, write: if false; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /middleware/authenticated.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware(async (to) => { 2 | const user = await getCurrentUser() 3 | if (!user) { 4 | return navigateTo({ path: '/login', query: { redirect: to.fullPath } }) 5 | } 6 | }) 7 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | app: { 4 | head: { 5 | titleTemplate: 'Emoji Panel - %s', 6 | link: [ 7 | { 8 | href: 'https://cdn.jsdelivr.net/npm/water.css@2/out/water.css', 9 | rel: 'stylesheet', 10 | }, 11 | ], 12 | }, 13 | }, 14 | 15 | css: ['~/assets/styles.css'], 16 | 17 | modules: ['nuxt-vuefire'], 18 | 19 | vuefire: { 20 | auth: true, 21 | config: { 22 | apiKey: 'AIzaSyBwmo761a-X3AV-2foLGWCpg2vTbrB7NjE', 23 | authDomain: 'emoji-panel-test-1.firebaseapp.com', 24 | projectId: 'emoji-panel-test-1', 25 | storageBucket: 'emoji-panel-test-1.appspot.com', 26 | messagingSenderId: '743661406627', 27 | appId: '1:743661406627:web:cd491cf645094383b33216', 28 | }, 29 | }, 30 | 31 | // TODO: maybe not needed for the demo 32 | typescript: { 33 | shim: false, 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview", 8 | "postinstall": "nuxt prepare" 9 | }, 10 | "dependencies": { 11 | "@vueuse/core": "^9.11.0", 12 | "firebase": "^9.15.0", 13 | "nuxt": "^3.0.0", 14 | "nuxt-vuefire": "^0.1.5", 15 | "vuefire": "^3.0.1" 16 | }, 17 | "devDependencies": { 18 | "@firebase/app-types": "^0.9.0", 19 | "firebase-admin": "^11.4.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 170 | 171 | 253 | -------------------------------------------------------------------------------- /pages/login.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 21 | 22 | 59 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | --------------------------------------------------------------------------------