+
diff --git a/src/pages/register.html b/src/pages/register.html
deleted file mode 100644
index b74d74f..0000000
--- a/src/pages/register.html
+++ /dev/null
@@ -1,89 +0,0 @@
-
diff --git a/src/sass/abstract/_breakpoints.sass b/src/sass/abstract/_breakpoints.sass
new file mode 100644
index 0000000..010d51e
--- /dev/null
+++ b/src/sass/abstract/_breakpoints.sass
@@ -0,0 +1,3 @@
+@use 'sass:map'
+
+$breakpoints: ('small': 530px, 'medium': 900px, 'large': 1024px)
\ No newline at end of file
diff --git a/src/sass/abstract/_index.sass b/src/sass/abstract/_index.sass
new file mode 100644
index 0000000..0e785f5
--- /dev/null
+++ b/src/sass/abstract/_index.sass
@@ -0,0 +1,3 @@
+@forward 'variables'
+@forward 'breakpoints'
+@forward 'mixins'
\ No newline at end of file
diff --git a/src/sass/abstracts/_mixins.sass b/src/sass/abstract/_mixins.sass
similarity index 58%
rename from src/sass/abstracts/_mixins.sass
rename to src/sass/abstract/_mixins.sass
index 190eedf..16f275c 100644
--- a/src/sass/abstracts/_mixins.sass
+++ b/src/sass/abstract/_mixins.sass
@@ -3,5 +3,5 @@
@mixin mq($key)
$size: map-get(b.$breakpoints, $key)
- @media only screen and (min-width: $size)
- @content
+ @media only screen and (max-width: $size)
+ @content
\ No newline at end of file
diff --git a/src/sass/abstract/_variables.sass b/src/sass/abstract/_variables.sass
new file mode 100644
index 0000000..b43cfcc
--- /dev/null
+++ b/src/sass/abstract/_variables.sass
@@ -0,0 +1,230 @@
+$clr-grey: #9c9c9c
+$clr-warn: #ff0000
+
+body
+ --clr-primary: #3b7845
+ --clr-primary-hover: #2f663a
+ --clr-primary-active: #22582d
+
+ --clr-primary-d-5: #2f663a
+ --clr-primary-d-10: #2a5e34
+ --clr-primary-d-15: #24562e
+ --clr-primary-d-20: #1f4e28
+ --clr-primary-d-25: #194621
+ --clr-primary-d-30: #133e1b
+ --clr-primary-d-35: #0e3615
+ --clr-primary-d-40: #082e0f
+ --clr-primary-d-45: #032608
+ --clr-primary-d-50: #000e02
+ --clr-primary-d-55: #000000
+ --clr-primary-d-60: #000000
+ --clr-primary-d-65: #000000
+ --clr-primary-d-70: #000000
+ --clr-primary-d-75: #000000
+ --clr-primary-d-80: #000000
+ --clr-primary-d-85: #000000
+ --clr-primary-d-90: #000000
+ --clr-primary-d-95: #000000
+
+ --clr-primary-l-5: #4c8e52
+ --clr-primary-l-10: #54965b
+ --clr-primary-l-15: #5cae63
+ --clr-primary-l-20: #64b66c
+ --clr-primary-l-25: #6ccf74
+ --clr-primary-l-30: #74d77d
+ --clr-primary-l-35: #7ce085
+ --clr-primary-l-40: #84e88e
+ --clr-primary-l-45: #8cf096
+ --clr-primary-l-50: #94f89f
+ --clr-primary-l-55: #9cfeb7
+ --clr-primary-l-60: #a4fec0
+ --clr-primary-l-65: #acffca
+ --clr-primary-l-70: #b4ffd3
+ --clr-primary-l-75: #bcffdb
+ --clr-primary-l-80: #c4ffe4
+ --clr-primary-l-85: #ccffec
+ --clr-primary-l-90: #d4fff5
+ --clr-primary-l-95: #dcffff
+
+ --clr-text: #1b1b1b
+ --clr-background: #ffffff
+
+ // Only used in Intro
+ --clr-text-light: #151111
+ --clr-slightly-visible: #bbbbbb
+ //
+
+ --clr-camera-background: #e9e9e9
+ --clr-camera-icon: #3b3b3b
+ --clr-camera-shutter: #c9c9c9
+
+ --clr-image-background: #e9e9e9
+ --clr-image-icon: #3b3b3b
+
+ --clr-message-background: #e9e9e9
+ --clr-message-text: #1b1b1b
+ --clr-message-time: #5e5e5e
+ --clr-message-myself-background: var(--clr-primary)
+ --clr-message-myself-text: #f5ebf2
+ --clr-message-myself-time: #cccccc
+
+ --clr-message-divider: #ffffff
+ --clr-message-divider-text: #1b1b1b
+
+ --clr-appbar-background: #e9e9e9
+ --clr-appbar-text: #1b1b1b
+ --clr-appbar-icon: #3b3b3b
+ --clr-appbar-icon-hover: #d4d4d4
+
+ --clr-search-background: #e9e9e9
+ --clr-search-text: #1b1b1b
+ --clr-search-text-placeholder: #848484
+
+ --clr-compose-background: #e9e9e9
+ --clr-compose-send-background: var(--clr-primary-l-5)
+ --clr-compose-send-background-hover: var(--clr-primary-hover)
+ --clr-compose-send-icon: #fff
+ --clr-compose-recording: #c5283a
+ --clr-compose-recording-icon: #151515
+ --clr-compose-icon: #3b3b3b
+ --clr-compose-text: #1b1b1b
+ --clr-compose-placeholder: #848484
+
+ --clr-setting-background: #e9e9e9
+ --clr-setting-border: #dedede
+ --clr-setting-theme-sun: #F48037
+ --clr-setting-theme-moon: #E1C38B
+ --clr-setting-button-bg: #f5f5f5
+ --clr-setting-button-bg-text: #151515
+ --clr-setting-button-bg-active: var(--clr-primary)
+ --clr-setting-button-bg-active-text: #fbfbfb
+ --clr-setting-grey: #F5F5F5
+ --clr-setting-reset: #2048b8
+ --clr-setting-accent-icon-selected: #fff
+ --clr-setting-accent-green: #3b7845
+ --clr-setting-accent-purple: #9932c8
+ --clr-setting-wallpaper-empty-icon: #fbfbfb
+ --clr-setting-icon: #3b3b3b
+ --clr-setting-text: #3b3b3b
+ --clr-setting-delete: #edcece
+ --clr-setting-delete-text: #B33A3A
+
+ --clr-scrollbar-track: #dfdfdf
+ --clr-scrollbar-thumb: #9c9c9c
+ --clr-scrollbar-thumb-hover: #acacac
+ --clr-scrollbar-thumb-active: #999999
+
+
+ &.theme--accent--purple
+ --clr-primary: #9932c8
+ --clr-primary-hover: #7e29a9
+ --clr-primary-active: #712597
+
+ --clr-primary-d-5: #7e29a9
+ --clr-primary-d-10: #752597
+ --clr-primary-d-15: #6c2190
+ --clr-primary-d-20: #631a89
+ --clr-primary-d-25: #5a1282
+ --clr-primary-d-30: #510b7b
+ --clr-primary-d-35: #480474
+ --clr-primary-d-40: #3f006d
+ --clr-primary-d-45: #360066
+ --clr-primary-d-50: #2d005f
+ --clr-primary-d-55: #240058
+ --clr-primary-d-60: #1b004f
+ --clr-primary-d-65: #120048
+ --clr-primary-d-70: #090041
+ --clr-primary-d-75: #00003a
+ --clr-primary-d-80: #000033
+ --clr-primary-d-85: #00002c
+ --clr-primary-d-90: #000025
+ --clr-primary-d-95: #00001e
+
+ --clr-primary-l-5: #a53bd1
+ --clr-primary-l-10: #b144d9
+ --clr-primary-l-15: #ba4de2
+ --clr-primary-l-20: #c356ea
+ --clr-primary-l-25: #cc5ff3
+ --clr-primary-l-30: #d568fb
+ --clr-primary-l-35: #df71ff
+ --clr-primary-l-40: #e87aff
+ --clr-primary-l-45: #f183ff
+ --clr-primary-l-50: #fb8cff
+ --clr-primary-l-55: #ff95ff
+ --clr-primary-l-60: #ff9eff
+ --clr-primary-l-65: #ffa7ff
+ --clr-primary-l-70: #ffb0ff
+ --clr-primary-l-75: #ffb9ff
+ --clr-primary-l-80: #ffc2ff
+ --clr-primary-l-85: #ffcbff
+ --clr-primary-l-90: #ffd4ff
+ --clr-primary-l-95: #ffddff
+
+ &.theme--dark
+ --clr-text: #fff
+ --clr-background: #121212
+
+ // Only used in Login
+ --clr-text-light: #fff
+ --clr-slightly-visible: #bbb
+ //
+
+ --clr-camera-background: #3b3b3b
+ --clr-camera-icon: #dedede
+ --clr-camera-shutter: #fbfbfb
+
+ --clr-image-background: #3b3b3b
+ --clr-image-icon: #dedede
+
+ --clr-message-background: #3b3b3b
+ --clr-message-text: #e9e9e9
+ --clr-message-time: #b1b1b1
+ --clr-message-myself-background: var(--clr-primary)
+ --clr-message-myself-text: #f5ebfa
+ --clr-message-myself-time: #cccccc
+
+ --clr-message-divider: #2e2e2e
+ --clr-message-divider-text: #e9e9e9
+
+ --clr-appbar-background: #2e2e2e
+ --clr-appbar-text: #f6f6f6
+ --clr-appbar-icon: #dedede
+ --clr-appbar-icon-hover: #525252
+
+ --clr-search-background: #2e2e2e
+ --clr-search-text: #e9e9e9
+ --clr-search-text-placeholder: #b9b9b9
+
+ --clr-compose-background: #2e2e2e
+ --clr-compose-send-background: var(--clr-primary-l-5)
+ --clr-compose-send-background-hover: var(--clr-primary-hover)
+ --clr-compose-send-icon: #dedede
+ --clr-compose-recording: #f23f42
+ --clr-compose-recording-icon: #151515
+ --clr-compose-icon: #dedede
+ --clr-compose-text: #e9e9e9
+ --clr-compose-placeholder: #b9b9b9
+
+ --clr-setting-background: #2e2e2e
+ --clr-setting-border: #5e5e5e
+ --clr-setting-theme-sun: #F48037
+ --clr-setting-theme-moon: #E1C38B
+ --clr-setting-button-bg: #3b3b3b
+ --clr-setting-button-bg-text: #fbfbfb
+ --clr-setting-button-bg-active: var(--clr-primary)
+ --clr-setting-button-bg-active-text: #fbfbfb
+ --clr-setting-grey: #3b3b3b
+ --clr-setting-reset: #6d81ff
+ --clr-setting-accent-icon-selected: #fbfbfb
+ --clr-setting-accent-green: #3b7845
+ --clr-setting-accent-purple: #9932c8
+ --clr-setting-wallpaper-empty-icon: #fff
+ --clr-setting-icon: #fbfbfb
+ --clr-setting-text: #fbfbfb
+ --clr-setting-delete: #e4acac
+ --clr-setting-delete-text: #a32222
+
+ --clr-scrollbar-track: #2e2e2e
+ --clr-scrollbar-thumb: #5e5e5e
+ --clr-scrollbar-thumb-hover: #7e7e7e
+ --clr-scrollbar-thumb-active: #9e9e9e
\ No newline at end of file
diff --git a/src/sass/abstracts/_breakpoints.sass b/src/sass/abstracts/_breakpoints.sass
deleted file mode 100644
index 6d3c344..0000000
--- a/src/sass/abstracts/_breakpoints.sass
+++ /dev/null
@@ -1,3 +0,0 @@
-@use 'sass:map'
-
-$breakpoints: ("small": 40em,"medium": 65em,"large": 80em)
\ No newline at end of file
diff --git a/src/sass/abstracts/_index.sass b/src/sass/abstracts/_index.sass
deleted file mode 100644
index 82d4bda..0000000
--- a/src/sass/abstracts/_index.sass
+++ /dev/null
@@ -1,3 +0,0 @@
-@forward 'variables'
-@forward 'mixins'
-@forward 'breakpoints'
\ No newline at end of file
diff --git a/src/sass/abstracts/_variables.sass b/src/sass/abstracts/_variables.sass
deleted file mode 100644
index f9f79d7..0000000
--- a/src/sass/abstracts/_variables.sass
+++ /dev/null
@@ -1,5 +0,0 @@
-$clr-primary: #fff
-$clr-secondary: #000
-$clr-highlight: #8b9e70
-
-$clr-warn: #ce5959
\ No newline at end of file
diff --git a/src/sass/base/_base.sass b/src/sass/base/_base.sass
index 9d47221..ff3ffb5 100644
--- a/src/sass/base/_base.sass
+++ b/src/sass/base/_base.sass
@@ -1,5 +1,26 @@
-@use '../abstracts/variables' as *
+html
+ font-size: 13px
+
+ &.text-size-s
+ font-size: 10px
+
+ &.text-size-l
+ font-size: 16px
body
- background-color: darken($clr-primary, 3%)
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"
\ No newline at end of file
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"
+
+@media (min-width: 768px)
+ html
+ font-size: calc(0.8125rem + ((1vw - 7.68px) * 0.2604))
+ min-height: 0vw
+
+@media (min-width: 1920px)
+ html
+ font-size: 16px
+
+ &.text-size-s
+ font-size: 13px
+
+ &.text-size-l
+ font-size: 19px
diff --git a/src/sass/base/_reset.sass b/src/sass/base/_reset.sass
index b82ba7f..b019cff 100644
--- a/src/sass/base/_reset.sass
+++ b/src/sass/base/_reset.sass
@@ -38,7 +38,7 @@ body
min-height: 100vh
text-rendering: optimizeSpeed
line-height: 1.5
-
+ background-clip: red
/* A elements that don't have a class get default styles */
a:not([class])
@@ -72,5 +72,4 @@ select
animation-duration: 0.01ms !important
animation-iteration-count: 1 !important
transition-duration: 0.01ms !important
- scroll-behavior: auto !important
-
+ scroll-behavior: auto !important
\ No newline at end of file
diff --git a/src/sass/components/_buttons.sass b/src/sass/components/_buttons.sass
deleted file mode 100644
index a3e4251..0000000
--- a/src/sass/components/_buttons.sass
+++ /dev/null
@@ -1,18 +0,0 @@
-@mixin button-theme($bg, $text)
- color: $text
- background-color: $bg
- border: none
- border-radius: 6px
- padding: 12px 0
- font-size: 1.2em
- font-weight: 600
- cursor: pointer
- text-decoration: none
- text-align: center
-
- &:hover
- background-color: darken($bg, 10%)
-
- &:focus
- background-color: lighten($bg, 10%)
-
\ No newline at end of file
diff --git a/src/sass/components/_form.sass b/src/sass/components/_form.sass
deleted file mode 100644
index 348545b..0000000
--- a/src/sass/components/_form.sass
+++ /dev/null
@@ -1,134 +0,0 @@
-@use '../abstracts/mixins' as m
-@use 'buttons' as *
-@use '../abstracts/variables' as *
-
-$background-color: #f3f3f3
-$white: #000
-$button: #fff
-$grey: #adadad
-$primary: red
-$primary-light: blue
-
-.test
- height: 100%
- display: flex
- justify-content: center
- align-items: center
-
- .form
- width: 100%
- max-width: 680px
- height: fit-content
- border-radius: 12px
- padding: 50px 35px
- background-color: $clr-primary
- position: relative
- box-shadow: rgba(136, 165, 191, 0.48) 6px 2px 16px 0px, rgba(255, 255, 255, 0.8) -6px -2px 16px 0px
-
- @include m.mq("small")
- width: 50%
-
- &__title
- text-align: center
- color: darken($clr-highlight, 25%)
- font-size: 2em
- font-weight: bold
- line-height: 2.2em
- margin-bottom: 42px
-
- &__group
- position: relative
- margin-bottom: 42px
- display: flex
- flex-direction: column
- align-items: center
-
- &:has(.form__group__error:not(:empty))
- margin-bottom: 12px
-
- &__input
- width: 100%
- padding: 10px
- border: 2px solid $clr-secondary
- background: $clr-primary
- border-radius: 5px
- outline: none
- color: $clr-secondary
- transition-duration: .5s
-
- &:hover,
- &:hover:not(:placeholder-shown) ~ .form__group__label
- border-color: lighten($clr-secondary, 65%)
-
- &:focus,
- &:focus:not(:placeholder-shown) ~ .form__group__label
- border-color: $clr-highlight
-
- &:focus ~ .form__group__label,
- &:not(:placeholder-shown) ~ .form__group__label
- transform: translateX(10px) translateY(-10px) scale(0.8)
- top: 0
- left: 12px
- padding-left: 6px
- padding-right: 6px
- background-color: $clr-primary
- letter-spacing: .2em
- border-left: 2px solid $clr-highlight
- border-right: 2px solid $clr-highlight
- color: $clr-highlight
-
- &:not(:placeholder-shown):not(:focus):not(:hover) ~ .form__group__label
- border-color: $clr-secondary
-
- &__icon
- position: absolute
- width: 30px
- height: 30px
- right: 6px
- top: 9px
- background-image: url(../assets/icons/password_show.svg)
- background-repeat: no-repeat
- background-position: center
- background-size: cover
- cursor: pointer
-
- &.active
- background-image: url(../assets/icons/password_hide.svg)
-
- &__label
- position: absolute
- left: 12px
- top: 12px
- pointer-events: none
- font-size: 1em
- color: $clr-secondary
- text-transform: uppercase
- transition-duration: .5s
-
- &__error
- width: 80%
- text-align: center
- background-color: lighten($clr-warn, 30%)
- margin-top: 12px
- padding: 6px
- border: 2px solid $clr-warn
- border-radius: 6px
- color: black
-
- &:empty
- display: none
-
- &__end
- margin-top: 50px
- width: 100%
- display: flex
- justify-content: space-between
- gap: 16px
-
- &__button
- width: 100%
- border: 1px solid lighten($clr-secondary, 65%) !important
- @include button-theme(darken($clr-primary,5%), darken($clr-highlight, 15%))
-
- &[type=submit]
- @include button-theme($clr-highlight, $clr-primary)
\ No newline at end of file
diff --git a/src/sass/components/_header.sass b/src/sass/components/_header.sass
deleted file mode 100644
index 5315fdb..0000000
--- a/src/sass/components/_header.sass
+++ /dev/null
@@ -1,29 +0,0 @@
-@import '../abstracts/variables'
-
-.header
- width: 100%
- height: fit-content
- display: flex
- flex-direction: row
- justify-content: start
- align-items: center
- background-color: $clr-highlight
- padding: 12px
- gap: 12px
- box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px
-
- &__back
- cursor: pointer
- width: 36px
- height: 36px
-
- &__container
- width: 100%
- display: flex
- flex-direction: row
- justify-content: space-between
- align-items: center
-
- &__title
- color: $clr-secondary
-
diff --git a/src/sass/components/_index.sass b/src/sass/components/_index.sass
index ac95d27..5c973f3 100644
--- a/src/sass/components/_index.sass
+++ b/src/sass/components/_index.sass
@@ -1,5 +1 @@
-@forward 'buttons'
-@forward 'form'
-@forward 'header'
-@forward 'message'
-@forward 'menuBtn'
+@forward 'message'
\ No newline at end of file
diff --git a/src/sass/components/_menuBtn.sass b/src/sass/components/_menuBtn.sass
deleted file mode 100644
index da188c6..0000000
--- a/src/sass/components/_menuBtn.sass
+++ /dev/null
@@ -1,117 +0,0 @@
-@use '../abstracts/variables' as *
-@use '../abstracts/mixins' as m
-@use 'buttons' as *
-
-.menuBtn
- display: flex
- flex-direction: column
- width: 50px
- cursor: pointer
-
- span
- background: $clr-secondary
- border-radius: 8px
- height: 4px
- margin: 3px 0
- transition: .4s cubic-bezier(0.68, -0.55, 0.265, 1.55)
-
- &:nth-child(1)
- width: 50%
-
- &:nth-child(2)
- width: 100%
-
- &:nth-child(3)
- width: 75%
-
- &.active
- span
- &:nth-child(1)
- transform-origin: bottom
- transform: rotatez(45deg) translate(4px,0px)
-
- &:nth-child(2)
- transform-origin: top
- transform: rotatez(-45deg)
-
- &:nth-child(3)
- transform-origin: bottom
- width:50%
- transform: translate(20px,-1px) rotatez(45deg)
-
- &:not(.active) ~ .chat__header__options
- max-height: 0px
- opacity: 0
-
-.chat__header__options
- box-shadow: 0 0 2rem rgba(black, 0.075), 0rem 1rem 1rem -1rem rgba(black, 0.1)
- padding: 12px
- border-radius: 12px
- position: fixed
- top: 80px
- right: 6px
- background-color: $clr-primary
- overflow: hidden
- width: calc( 100% - 12px )
- max-height: 600px
- opacity: 1
- transition: all .3s ease-in-out
-
- @include m.mq("small")
- width: 50%
- max-width: 250px
-
- &__element
- border-bottom: 1px solid $clr-secondary
- width: 100%
- margin-bottom: 12px
- padding-bottom: 6px
-
- .element__title
- color: $clr-secondary
- font-size: 1.2em
- font-weight: bold
- line-height: 1.2em
- margin-bottom: 6px
-
- .element__content
- &.theme
- width: 100%
- display: flex
- flex-direction: row
- justify-content: space-around
- align-items: center
- gap: 12px
-
- button
- @include button-theme($clr-highlight, $clr-secondary)
- width: 100%
-
- &.current
- @include button-theme(darken($clr-highlight, 15%), $clr-secondary)
- outline-offset: 2px
- outline: 2px solid $clr-secondary
-
- &.fontsize
- input
- width: 100%
- appearance: none
- height: 10px
- outline: none
- background: lighten($clr-secondary, 75%)
- border-radius: 5px
-
- &::-webkit-slider-thumb
- appearance: none
- cursor: pointer
- width: 24px
- height: 24px
- border-radius: 12px
- background: $clr-highlight
-
-
- &:last-child
- margin-bottom: 0
-
-#deregister
- cursor: pointer
\ No newline at end of file
diff --git a/src/sass/components/_message.sass b/src/sass/components/_message.sass
index 184e9f5..fbf4413 100644
--- a/src/sass/components/_message.sass
+++ b/src/sass/components/_message.sass
@@ -1,29 +1,113 @@
-@use '../abstracts/variables' as *
+@use '../abstract/variables' as *
+@use '../abstract/mixins' as m
.message
- background-color: $clr-primary
- box-shadow: 0 0 2rem rgba(black, 0.075), 0rem 1rem 1rem -1rem rgba(black, 0.1)
- padding: 12px
- margin-bottom: 18px
- max-width: 66%
+ position: relative
+ background-color: var(--clr-message-background)
width: fit-content
- border-radius: 1.125rem 1.125rem 1.125rem 0
+ max-width: 60%
+ padding: .8rem 1rem
+ border-radius: 1.55rem
+ margin-top: 1rem
+ display: flex
+ flex-direction: column
+ gap: .2rem
- &__title
+ &.emoji_only
+ background-color: transparent
+
+ .message__header__sender
+ margin-bottom: 1.5rem
+
+ .message__body__text__content
+ font-size: 4rem
+
+ .message__body__text__time
+ bottom: -2rem
+
+ &__header
display: flex
flex-direction: row
justify-content: space-between
- align-items: flex-end
- gap: 12px
+ align-items: end
+ gap: .4rem
+
+ &__sender
+ font-size: .9rem
+ font-weight: 800
+
+ &__body
+ &__text
+ &__content
+ color: var(--clr-message-text)
+ font-size: 1.1rem
+ line-height: 1.5rem
+ overflow-wrap: break-all
+ // word-wrap: break-all
+ word-break: break-word
+ // hyphens: auto
+
+ &__time
+ position: relative
+ bottom: -0.5rem
+ float: right
+ margin-left: .5rem
+ font-size: .8rem
+ font-weight: 400
+ line-height: 1.2rem
+ color: var(--clr-message-time)
+
+ &__image
+ width: 100%
+ border-radius: 1.6rem
+ margin-top: .5rem
+ margin-bottom: .5rem
+ cursor: pointer
+
+ &:not([src])
+ display: none
+
+ &.myself
+ background-color: var(--clr-primary)
+ color: var(--clr-message-myself-text)
+ border-bottom-right-radius: .4rem
+ margin-left: auto
+ padding-right: 1rem
+
+ .message__body__text__content
+ color: var(--clr-message-myself-text)
+
+ .message__body__text__time
+ color: var(--clr-message-myself-time)
+
+ &.emoji_only
+ background-color: transparent
+
+ .message__header
+ display: none
+
+ &.same_sender
+ border-bottom-right-radius: .4rem
+ border-top-right-radius: .4rem
+ margin-top: .2rem
+
+ &:not(.myself)
+ border-bottom-left-radius: .4rem
+ padding-left: 1rem
+
+ &.same_sender
+ border-bottom-left-radius: .4rem
+ border-top-left-radius: .4rem
+ margin-top: .2rem
- &:last-child
- margin-bottom: 0
+ &.same_sender
+ .message__header
+ display: none
-.myself
- background-color: $clr-highlight
- margin: 0 0 0 auto
- margin-bottom: 18px
- border-radius: 1.125rem 1.125rem 0 1.125rem
+@include m.mq('medium')
+ .message
+ max-width: 75%
- .message__content
- text-align: end
\ No newline at end of file
+@include m.mq('small')
+ .message
+ max-width: 90%
\ No newline at end of file
diff --git a/src/sass/layout/_chat.sass b/src/sass/layout/_chat.sass
deleted file mode 100644
index 3aba368..0000000
--- a/src/sass/layout/_chat.sass
+++ /dev/null
@@ -1,88 +0,0 @@
-@import '../abstracts/variables'
-@import '../components/buttons'
-
-.chat
- width: 100%
- height: 100%
- display: flex
- flex-direction: column
- align-items: center
-
- &__window
- height: 100%
- width: 100%
- padding: 12px
- overflow-y: scroll
- padding-bottom: 120px
-
- &::-webkit-scrollbar
- width: 20px
-
-
- &::-webkit-scrollbar-track
- background-color: transparent
-
-
- &::-webkit-scrollbar-thumb
- background-color: lighten($clr-secondary, 90%)
- border-radius: 20px
- border: 6px solid transparent
- background-clip: content-box
-
-
- &::-webkit-scrollbar-thumb:hover
- background-color: lighten($clr-secondary, 80%)
-
-
- &__input
- width: 96%
- margin: 12px auto 12px auto
- border-radius: 18px
- height: fit-content
- display: flex
- flex-direction: row
- justify-content: space-between
- background-color: $clr-primary
- padding: 12px
- gap: 18px
- position: fixed
- bottom: 0
- right: 0
- left: 0
- box-shadow: rgba(136, 165, 191, 0.48) 6px 2px 16px 0px, rgba(255, 255, 255, 0.8) -6px -2px 16px 0px
-
- &__text
- width: 100%
- resize: none
- padding: 18px 12px
- background-color: transparent
- border: none
-
- &::placeholder
- color: lighten($clr-secondary, 40%)
-
- &:focus
- outline: none
-
- &::-webkit-scrollbar
- width: 20px
-
-
- &::-webkit-scrollbar-track
- background-color: transparent
-
-
- &::-webkit-scrollbar-thumb
- background-color: lighten($clr-secondary, 90%)
- border-radius: 20px
- border: 6px solid transparent
- background-clip: content-box
-
-
- &::-webkit-scrollbar-thumb:hover
- background-color: lighten($clr-secondary, 80%)
-
- &__send
- @include button-theme($clr-highlight, $clr-primary)
- padding-left: 24px
- padding-right: 24px
\ No newline at end of file
diff --git a/src/sass/layout/_index.sass b/src/sass/layout/_index.sass
deleted file mode 100644
index e889faa..0000000
--- a/src/sass/layout/_index.sass
+++ /dev/null
@@ -1 +0,0 @@
-@forward 'chat'
\ No newline at end of file
diff --git a/src/sass/main.sass b/src/sass/main.sass
index 284961b..6a00c81 100644
--- a/src/sass/main.sass
+++ b/src/sass/main.sass
@@ -1,4 +1,4 @@
-@use "abstracts"
-@use "base"
-@use "components"
-@use "layout"
\ No newline at end of file
+@use 'base'
+@use 'pages'
+@use 'abstract'
+@use 'components'
\ No newline at end of file
diff --git a/src/sass/pages/_chat.sass b/src/sass/pages/_chat.sass
new file mode 100644
index 0000000..15dce2e
--- /dev/null
+++ b/src/sass/pages/_chat.sass
@@ -0,0 +1,745 @@
+@use '../abstract/variables' as *
+
+*
+ &::-webkit-scrollbar
+ width: 0.5rem
+
+ &::-webkit-scrollbar-track
+ background-color: var(--clr-scrollbar-track)
+ border-radius: .25rem
+
+ &::-webkit-scrollbar-thumb
+ background-color: var(--clr-scrollbar-thumb)
+ background-clip: content-box
+ border-radius: .25rem
+
+ &::-webkit-scrollbar-thumb:hover
+ background-color: var(--clr-scrollbar-thumb-hover)
+
+ &::-webkit-scrollbar-thumb:active
+ background-color: var(--clr-scrollbar-thumb-active)
+
+
+.chat
+ position: relative
+ width: 100%
+ height: 100vh
+ overflow: hidden
+ display: flex
+ flex-direction: column
+
+ &__app-bar
+ position: fixed
+ z-index: 100
+ width: 100%
+ height: fit-content
+ background-color: var(--clr-appbar-background)
+ padding: 1.5rem
+ display: flex
+ flex-direction: row
+ gap: 1.5rem
+ align-items: center
+ justify-content: space-between
+ box-shadow: 0 2px 8px 0 rgba(99, 99, 99, 0.2)
+
+ &__container
+ width: 100%
+ display: flex
+ flex-direction: row
+ align-items: center
+ justify-content: space-between
+
+ .title
+ color: var(--clr-appbar-text)
+ font-size: 2rem
+ font-weight: 700
+
+ .search
+ width: 2.5rem
+ height: 2.5rem
+ border-radius: 0.4rem
+ cursor: pointer
+ transition: background-color .2s ease-in-out
+
+ &:hover
+ background-color: var(--clr-appbar-icon-hover)
+
+ .icon
+ width: 100%
+ height: 100%
+ background-color: var(--clr-appbar-icon)
+ mask: url(../../assets/icons/search.svg) no-repeat center
+ -webkit-mask: url(../../assets/icons/search.svg) no-repeat center
+ mask-size: 90%
+ -webkit-mask-size: 90%
+
+ &__settings
+ width: 2.5rem
+ height: 2.5rem
+ cursor: pointer
+ transition: background-color .2s ease-in-out
+ border-radius: 0.4rem
+
+ &:hover
+ background-color: var(--clr-appbar-icon-hover)
+
+ .icon
+ width: 100%
+ height: 100%
+ background-color: var(--clr-appbar-icon)
+ mask: url(../../assets/icons/more.svg) no-repeat center
+ -webkit-mask: url(../../assets/icons/more.svg) no-repeat center
+ mask-size: 90%
+ -webkit-mask-size: 90%
+
+ &__window
+ width: 100%
+ height: 100%
+ margin-top: 5.715rem
+ background-color: var(--clr-background)
+ background-size: cover
+ background-position: center
+ background-repeat: no-repeat
+ padding: 1.5rem 1.5rem 7rem 1.5rem
+ display: flex
+ flex-direction: column-reverse
+ overflow-y: scroll
+ overflow-x: hidden
+
+ &__divider
+ width: fit-content
+ background-color: var(--clr-message-divider)
+ color: var(--clr-message-divider-text)
+ padding: .4rem 1rem
+ border-radius: 2rem
+ font-weight: 700
+ margin: 1rem auto 0 auto
+
+ &:last-child
+ margin-top: 0
+
+ &__preview
+ position: absolute
+ width: 10rem
+ height: 7rem
+ border-radius: 1.5rem
+ background-color: var(--clr-primary)
+ left: 3rem
+ bottom: 5.5rem
+ margin-bottom: 1rem
+
+ &:has(.chat__preview__container__image:not([src]))
+ display: none
+
+ &__container
+ position: relative
+ width: 100%
+ height: 100%
+
+ &__image
+ width: 100%
+ height: 100%
+ border-radius: 1.5rem
+
+ &__remove
+ position: absolute
+ top: .5rem
+ right: .5rem
+ width: 2rem
+ height: 2rem
+ cursor: pointer
+ background-image: url(../../assets/icons/close.svg)
+ background-size: cover
+ background-position: center
+ background-repeat: no-repeat
+ transition: transform .4s ease-in-out
+
+ &__compose
+ position: fixed
+ bottom: 0
+ width: calc( 100% - 3rem )
+ min-height: 4rem
+ height: fit-content
+ display: flex
+ flex-direction: row
+ align-items: center
+ gap: 1.5rem
+ margin: 0 1.5rem 1.5rem 1.5rem
+
+ .compose
+ &__container
+ width: calc( 100% - 6rem )
+ background-color: var(--clr-compose-background)
+ box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px
+ border-radius: 2rem
+ padding: .5rem 1rem
+ display: flex
+ flex-direction: row
+ align-items: center
+ gap: .75rem
+
+ &__camera
+ width: 2.5rem
+ height: 2.5rem
+ cursor: pointer
+
+ &__icon
+ width: 100%
+ height: 100%
+ background-color: var(--clr-compose-icon)
+ mask: url(../../assets/icons/camera.svg) no-repeat center
+ -webkit-mask: url(../../assets/icons/camera.svg) no-repeat center
+ mask-size: 100%
+ -webkit-mask-size: 100%
+
+ &__microphone
+ width: 2.5rem
+ height: 2.5rem
+ cursor: pointer
+
+ &__icon
+ width: 100%
+ height: 100%
+ background-color: var(--clr-compose-icon)
+ mask: url(../../assets/icons/microphone.svg) no-repeat center
+ -webkit-mask: url(../../assets/icons/microphone.svg) no-repeat center
+ mask-size: 100%
+ -webkit-mask-size: 100%
+
+ @keyframes recording
+ 0%
+ transform: scale(1)
+ 25%
+ transform: scale(1.05)
+ 50%
+ transform: scale(1)
+ 75%
+ transform: scale(.95)
+ 100%
+ transfform: scale(1)
+
+ &.recording
+ padding: .3rem
+ background-color: var(--clr-compose-recording)
+ border-radius: 1.5rem
+ animation: recording 1s infinite
+
+ .compose__container__microphone__icon
+ background-color: var(--clr-compose-recording-icon)
+
+ &:hover
+ .compose__container__microphone__icon
+ background-color: var(--clr-compose-recording-icon)
+ mask-image: url(../../assets/icons/microphone-off.svg)
+ -webkit-mask-image: url(../../assets/icons/microphone-off.svg)
+
+ &__input
+ width: 100%
+ resize: none
+ color: var(--clr-compose-text)
+ background-color: transparent
+ border: none
+ font-size: 1.2rem
+ font-weight: 400
+
+ &::placeholder
+ font-size: 1.2rem
+ color: var(--clr-compose-placeholder)
+
+ &:focus
+ outline: none
+
+ &__send
+ width: 3.5rem
+ height: 3.5rem
+ border-radius: 2rem
+ border: none
+ background-color: var(--clr-compose-send-background)
+ cursor: pointer
+ box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px
+ transition: background-size .4s ease-in-out, background-color .2s ease-in-out
+
+ &:hover
+ background-color: var(--clr-compose-send-background-hover)
+
+ &__icon
+ width: 100%
+ height: 100%
+ background-color: var(--clr-compose-send-icon)
+ transform: rotate(-35deg)
+ mask: url(../../assets/icons/send.svg) no-repeat 2px center
+ -webkit-mask: url(../../assets/icons/send.svg) no-repeat 2px center
+ mask-size: 100%
+ -webkit-mask-size: 100%
+
+.searchbar
+ position: absolute
+ top: 4.75rem
+ right: 5rem
+ width: 25rem
+ height: 4rem
+ max-width: 0px
+ border-radius: 1.2rem
+ background-color: var(--clr-search-background)
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px
+ display: flex
+ flex-direction: row
+ align-items: center
+ justify-content: center
+ opacity: 0
+ pointer-events: none
+ transition: max-width .8s ease-in-out
+
+ &.active
+ opacity: 1
+ pointer-events: all
+ max-width: 100rem
+
+
+ input
+ width: calc( 100% - 1rem )
+ height: calc( 100% - 1rem )
+ border: none
+ outline: none
+ background-color: transparent
+ font-size: 1.2rem
+ font-weight: 500
+ color: var(--clr-search-text)
+
+ &:placeholder
+ color: var(--clr-search-text-placeholder)
+
+.options
+ position: absolute
+ width: 25rem
+ right: 1.5rem
+ top: 4.75rem
+ border-radius: 1.2rem
+ color: var(--clr-setting-text)
+ background-color: var(--clr-setting-background)
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px
+ max-height: 0px
+ overflow: hidden
+ transition: max-height .4s ease-in-out
+
+ &.active
+ max-height: 85vh
+
+ &__container
+ position: relative
+ max-height: 85vh
+ overflow-y: auto
+ overflow-x: hidden
+
+ &__section
+ padding: 1rem
+ border-bottom: 1px solid var(--clr-setting-border)
+
+ &:last-child
+ border-bottom: none
+
+ &__header
+ display: flex
+ flex-direction: row
+ align-items: center
+ justify-content: start
+ gap: 1rem
+ margin-bottom: 1rem
+
+ &.button
+ cursor: pointer
+ margin-bottom: 0
+
+ .icon
+ width: 2rem
+ height: 2rem
+ background-color: var(--clr-setting-icon)
+
+ &.help
+ mask: url(../../assets/icons/help.svg) no-repeat center
+ -webkit-mask: url(../../assets/icons/help.svg) no-repeat center
+ mask-size: 100%
+ -webkit-mask-size: 100%
+
+ &.settings
+ mask: url(../../assets/icons/settings.svg) no-repeat center
+ -webkit-mask: url(../../assets/icons/settings.svg) no-repeat center
+ mask-size: 100%
+ -webkit-mask-size: 100%
+
+ &.logout
+ mask: url(../../assets/icons/logout.svg) no-repeat center
+ -webkit-mask: url(../../assets/icons/logout.svg) no-repeat center
+ mask-size: 100%
+ -webkit-mask-size: 100%
+
+ h3
+ font-size: 1.2rem
+ font-weight: 700
+ color: var(--clr-text)
+
+ &__wrapper
+ display: flex
+ flex-direction: column
+ gap: 1.2rem
+
+ .setting
+ display: flex
+ flex-direction: row
+ align-items: center
+ justify-content: space-between
+ gap: 1rem
+
+ &__description
+ position: relative
+ width: 100%
+
+ h5
+ margin: 0
+ font-size: 1rem
+ color: var(--clr-text)
+
+ p
+ margin: 0
+ font-size: 1rem
+ color: var(--clr-text)
+
+ &__reset
+ position: absolute
+ top: 0
+ right: 0
+ color: var(--clr-setting-reset)
+ cursor: pointer
+ font-size: .9rem
+
+ .fontSize__container
+ width: 100%
+ display: flex
+ flex-direction: row
+ align-items: center
+ gap: .5rem
+
+ button
+ padding: 1rem 1.5rem
+ width: 100%
+ border-radius: 2rem
+ border: none
+ outline: none
+ cursor: pointer
+ background-color: var(--clr-setting-button-bg)
+ color: var(--clr-setting-button-bg-text)
+ font-weight: 500
+
+ &.active
+ background-color: var(--clr-setting-button-bg-active)
+ color: var(--clr-setting-button-bg-active-text)
+ font-weight: 700
+
+ .theme
+ &__container
+ width: fit-content
+ height: 100%
+ display: flex
+ flex-direction: row
+ align-items: center
+ justify-content: center
+
+ &__switch
+ display: inline-block
+ position: relative
+ width: 5.5rem
+ height: 2.5rem
+ border-radius: 1.25rem
+ overflow: hidden
+ cursor: pointer
+
+ input
+ opacity: 0
+ width: 0
+ height: 0
+
+ &__slider
+ position: absolute
+ background-color: var(--clr-setting-grey)
+ top: 0
+ left: 0
+ bottom: 0
+ right: 0
+ border-radius: 1.25rem
+ transition: all 0.4s
+ cursor: pointer
+
+ &::before
+ position: absolute
+ content: ""
+ background-color: var(--clr-setting-theme-sun)
+ height: 2rem
+ width: 2rem
+ border-radius: 50%
+ top: 50%
+ left: 0.25rem
+ transform: translateY(-50%)
+ transition: all 0.4s
+
+ &::after
+ position: absolute
+ content: ""
+ background-color: var(--clr-setting-grey)
+ height: 2rem
+ width: 2rem
+ border-radius: 50%
+ top: 30%
+ left: -2rem
+ transform: translateY(-50%)
+ transition: all 0.4s
+
+ input:checked ~ .theme__container__switch__slider::before
+ transform: translate(150%, -50%)
+ background-color: var(--clr-setting-theme-moon)
+
+ input:checked ~ .theme__container__switch__slider::after
+ transform: translate(230%, -50%)
+
+ .deleteAccount__container
+ display: flex
+ flex-direction: column
+ align-items: center
+ gap: .5rem
+
+ button
+ padding: 1rem 1.5rem
+ width: 100%
+ border-radius: 2rem
+ border: none
+ outline: none
+ cursor: pointer
+ background-color: var(--clr-setting-delete)
+ color: var(--clr-setting-delete-text)
+ font-weight: 700
+
+ .primaryColor__container
+ display: flex
+ flex-direction: row
+ align-items: center
+ gap: .5rem
+
+ button
+ background-color: red
+ height: 2.5rem
+ width: 2.5rem
+ border-radius: 50%
+ border: none
+ outline: none
+ cursor: pointer
+ border: solid .25rem var(--clr-setting-grey)
+
+ div
+ display: none
+
+ &[data-color="green"]
+ background-color: var(--clr-setting-accent-green)
+
+ &[data-color="purple"]
+ background-color: var(--clr-setting-accent-purple)
+
+ &.active
+ div
+ display: block
+ width: 100%
+ height: 100%
+ background-color: var(--clr-setting-accent-icon-selected)
+ mask: url(../../assets/icons/done.svg) no-repeat center
+ -webkit-mask: url(../../assets/icons/done.svg) no-repeat center
+ mask-size: 100%
+ -webkit-mask-size: 100%
+
+ .wallpaper__container
+ width: 100%
+ display: flex
+ flex-direction: row
+ align-items: center
+ gap: .5rem
+
+ &__preview
+ position: relative
+ width: 100%
+ padding-bottom: 25%
+ border-radius: 1rem
+ background-color: var(--clr-primary)
+ background-size: cover
+ cursor: pointer
+
+ .icon
+ position: absolute
+ display: none
+ width: 100%
+ height: 100%
+
+ &.empty
+ .icon
+ display: block
+ background-color: var(--clr-setting-wallpaper-empty-icon)
+ mask: url(../../assets/icons/wallpaper.svg) no-repeat center
+ -webkit-mask: url(../../assets/icons/wallpaper.svg) no-repeat center
+ mask-size: 60%
+ -webkit-mask-size: 60%
+
+ &__upload
+ position: relative
+ width: 100%
+ padding-bottom: 25%
+ border-radius: 1rem
+
+ input
+ display: none
+
+ label
+ position: absolute
+ display: inline-block
+ width: 100%
+ height: 100%
+ cursor: pointer
+ border-radius: 1rem
+ background-color: var(--clr-setting-grey)
+
+ .wallpaper__upload__icon
+ width: 100%
+ height: 100%
+ background-color: var(--clr-setting-icon)
+ mask: url(../../assets/icons/upload.svg) no-repeat center
+ -webkit-mask: url(../../assets/icons/upload.svg) no-repeat center
+ mask-size: 40%
+ -webkit-mask-size: 40%
+
+ .setting-column
+ align-items: start
+ flex-direction: column
+ gap: .6rem
+
+
+
+
+#camera-modal
+ position: relative
+ margin: auto
+ padding: 2rem
+ border-radius: 3.3rem
+ max-width: 1080px
+ height: 640px
+ border: none
+ overflow: hidden
+ background-color: var(--clr-camera-background)
+
+ &::backdrop
+ background-color: rgba(0, 0, 0, 0.27)
+
+ .camera
+ width: 100%
+ height: 100%
+ display: flex
+ flex-direction: column
+ gap: 1rem
+ justify-content: space-between
+ align-items: center
+
+ &__close
+ position: absolute
+ width: 2.5rem
+ height: 2.5rem
+ top: .8rem
+ right: .8rem
+ cursor: pointer
+ background-image: url(../../assets/icons/close.svg)
+ background-size: cover
+ background-position: center
+ background-repeat: no-repeat
+ transition: transform 0.4s ease-in-out
+
+ &__preview
+ width: 100%
+ height: 100%
+ max-height: calc(100% - 5rem)
+ border-radius: 3.3rem
+
+ &__options
+ width: 100%
+ display: flex
+ flex-direction: row
+ justify-content: space-around
+ align-items: center
+
+ &__shutter
+ position: relative
+ width: 4rem
+ height: 4rem
+ border-radius: 2rem
+ background-color: transparent
+ outline: none
+ border: none
+
+ .circle
+ position: absolute
+ top: 12%
+ left: 12%
+ bottom: 12%
+ right: 12%
+ border-radius: 100%
+ background-color: var(--clr-camera-shutter)
+ opacity: 0.8
+ transition: all 0.25s
+
+ .ring
+ position: absolute
+ top: 0
+ left: 0
+ bottom: 0
+ right: 0
+ border-radius: 100%
+ border: 0.25rem solid var(--clr-camera-shutter)
+ opacity: 0.8
+ transition: all 0.25s
+
+ &:hover .circle
+ opacity: 1
+
+ &:active .ring
+ opacity: 1
+
+ &:active .circle
+ opacity: 0.6
+
+#image-modal
+ position: relative
+ margin: auto
+ padding: 3rem
+ border-radius: 3.3rem
+ border: none
+ overflow: hidden
+ outline: none
+ background-color: var(--clr-image-background)
+
+ &::backdrop
+ background-color: rgba(0, 0, 0, 0.27)
+
+ .image
+ position: relative
+ margin: 0
+
+ &__close
+ position: absolute
+ right: -2rem
+ top: -2rem
+ width: 2.5rem
+ height: 2.5rem
+ cursor: pointer
+ background-image: url(../../assets/icons/close.svg)
+ background-size: cover
+ background-position: center
+ background-repeat: no-repeat
+ transition: transform 0.4s ease-in-out
+
+ &__preview
+ height: 100%
+ border: none
+ outline: none
+ overflow: hidden
\ No newline at end of file
diff --git a/src/sass/pages/_index.sass b/src/sass/pages/_index.sass
new file mode 100644
index 0000000..1113806
--- /dev/null
+++ b/src/sass/pages/_index.sass
@@ -0,0 +1,2 @@
+@forward 'login'
+@forward 'chat'
\ No newline at end of file
diff --git a/src/sass/pages/_login.sass b/src/sass/pages/_login.sass
new file mode 100644
index 0000000..9b9bf1f
--- /dev/null
+++ b/src/sass/pages/_login.sass
@@ -0,0 +1,294 @@
+@use '../abstract/mixins' as m
+@import '../abstract/variables'
+
+.login
+ width: 100%
+ height: 100vh
+ overflow: hidden
+ background-color: var(--clr-primary-l-30)
+ padding: 2rem
+ display: flex
+ justify-content: center
+ align-items: center
+
+ &__container
+ position: relative
+ width: 100%
+ max-width: 1080px
+ min-height: 640px
+ background-color: var(--clr-background)
+ border-radius: 3.4rem
+ box-shadow: 0 60px 40px -30px rgba(0, 0, 0, 0.27)
+
+ &__box
+ position: absolute
+ width: calc(100% - 4.2rem)
+ height: calc(100% - 4.2rem)
+ top: 50%
+ left: 50%
+ transform: translate(-50%, -50%)
+
+ &.signup-mode
+ .forms
+ left: 55%
+
+ .intro
+ left: 0
+
+ .forms__signin
+ opacity: 0
+ pointer-events: none
+
+ .forms__signup
+ opacity: 1
+ pointer-events: all
+
+
+.forms
+ position: absolute
+ height: 100%
+ width: 45%
+ top: 0
+ left: 0
+ display: grid
+ grid-template-columns: 1fr
+ grid-template-rows: 1fr
+ transition: .8s ease-in-out
+
+ &__signin,
+ &__signup
+ max-width: 320px
+ width: 100%
+ height: 100%
+ margin: 0 auto
+ display: flex
+ flex-direction: column
+ justify-content: space-evenly
+ grid-column: 1 / 2
+ grid-row: 1 / 2
+ transition: opacity .02s .4s
+
+ &__heading
+ h2
+ font-size: 3.2rem
+
+ h6
+ font-size: 1.6rem
+ color: var(--clr-text-light)
+ font-weight: 400
+ display: inline
+
+ .forms__toggle
+ color: var(--clr-primary)
+ text-decoration: none
+ font-size: 1.2rem
+ font-weight: 600
+
+ &:hover
+ color: var(--clr-primary-hover)
+
+ &__signup
+ opacity: 0
+ pointer-events: none
+
+.intro
+ position: absolute
+ height: 100%
+ width: 55%
+ top: 0
+ left: 45%
+ background-color: var(--clr-primary-l-25)
+ border-radius: 2rem
+ transition: 0.8s ease-in-out
+ padding: 1.8rem
+ display: flex
+ flex-direction: column
+ justify-content: space-evenly
+ align-items: center
+
+ &__message
+ text-align: center
+
+ h1
+ font-size: 3.8rem
+ font-weight: 900
+ color: var(--clr-text)
+
+ h4
+ font-size: 1.4rem
+ font-weight: 400
+ color: var(--clr-text-light)
+
+ &__image
+ width: 80%
+ height: 80%
+ background-image: url(../../assets/icons/hero_image.svg)
+ background-size: cover
+ background-position: center
+ background-repeat: no-repeat
+
+
+.form-content
+ &__input-wrap
+ position: relative
+ height: 37px
+ margin-bottom: 1.4rem
+ display: flex
+ align-items: center
+
+ &__input-field
+ position: absolute
+ width: 100%
+ height: 100%
+ border: none
+ outline: none
+ background-color: transparent
+ border-bottom: 2px solid var(--clr-slightly-visible)
+ padding: 0
+ font-size: 1.2rem
+ color: var(--clr-text)
+ transition: .3s
+
+ &:focus
+ border-bottom: 2px solid var(--clr-primary)
+
+ &:focus ~ .form-content__input-wrap__input-label
+ color: var(--clr-primary)
+
+ &:focus ~ .form-content__input-wrap__input-label,
+ &:not(:placeholder-shown) ~ .form-content__input-wrap__input-label
+ transform: translateY(-180%)
+ font-size: .9rem,
+
+ &__input-label
+ position: absolute
+ left: 0
+ top: 50%
+ transform: translateY(-50%)
+ font-size: 1.2rem
+ color: var(--clr-slightly-visible)
+ pointer-events: none
+ transition: .4s
+
+ .form-content__password-toggle
+ position: absolute
+ width: 30px
+ height: 30px
+ right: 0
+ top: 50%
+ transform: translateY(-50%)
+ cursor: pointer
+ background-image: url("../../assets/icons/password_show.svg")
+ background-size: cover
+ background-position: center
+ background-repeat: no-repeat
+
+ &.show-password
+ background-image: url("../../assets/icons/password_hide.svg")
+
+ &__checkbox
+ width: 1.2rem
+ height: 1.2rem
+ accent-color: var(--clr-text-light)
+ margin-right: .6rem
+ border: 1px solid var(--clr-slightly-visible)
+ cursor: pointer
+
+ &__submit-btn
+ display: inline-block
+ width: 100%
+ height: 43px
+ background-color: var(--clr-text-light)
+ color: var(--clr-background)
+ border: none
+ outline: none
+ cursor: pointer
+ border-radius: 21.5px
+ font-size: 1.2rem
+ margin-bottom: 2rem
+ transition: .3s
+
+ &:hover
+ background-color: var(--clr-primary)
+
+@include m.mq("medium")
+ .login__container
+ height: auto
+ max-width: 550px
+ overflow: hidden
+
+ &__box
+ position: static
+ transform: none
+ width: revert
+ height: revert
+ padding: 2rem
+ display: flex
+ flex-direction: column-reverse
+
+ .forms
+ position: revert
+ width: 100%
+ height: auto
+
+ &__signin,
+ &__signup
+ max-width: revert
+ padding: 1.5rem 2.5rem 2rem
+ transition: transform 0.8s ease-in-out, opacity 0.45s linear
+
+ &__heading
+ margin-bottom: 1rem
+
+ &__signup
+ transform: translateX(100%)
+
+ .intro
+ position: revert
+ width: 100%
+ height: auto
+ padding: 2rem 1rem
+
+ &__image
+ display: none
+
+ &.signup-mode
+ .forms__signin
+ transform: translateX(-100%)
+
+ .forms__signup
+ transform: translateX(0%)
+
+@include m.mq("small")
+ .login
+ padding: 1.2rem
+
+ &__container
+ border-radius: 2rem
+
+ &__box
+ padding: 1rem
+
+ &__container
+ .intro
+ padding: 1.5rem 1rem
+
+ h1
+ font-size: 3rem
+
+ h4
+ font-size: 1.2rem
+
+ .forms__signin,
+ .forms__signup
+ padding: 1rem 2rem 1.5rem
+
+ &__heading
+ h2
+ font-size: 2.6em
+
+ h6
+ font-size: 1rem
+
+ .forms__toggle
+ font-size: 1rem
\ No newline at end of file
diff --git a/src/sw.ts b/src/sw.ts
index e75c18b..7debfc5 100644
--- a/src/sw.ts
+++ b/src/sw.ts
@@ -1,166 +1,92 @@
-import { ApiService } from './ts/_service/api.service';
-import { IndexedDBManager } from './ts/_service/storage.service';
-const _apiService = new ApiService();
-
const CACHE_VERSION = {
- STATIC: '6',
- DYNAMIC: '3'
+ STATIC: '2',
+ DYNAMIC: '2',
+ MINOR: '164'
};
const CACHE_LIST = {
- STATIC_CACHE: `static-pwa-chat-${CACHE_VERSION.STATIC}`,
- DYNAMIC_CACHE: `dynamic-pwa-chat-${CACHE_VERSION.DYNAMIC}`
+ STATIC_CACHE: `static-pwa-chat-${CACHE_VERSION.STATIC}.${CACHE_VERSION.MINOR}`,
+ DYNAMIC_CACHE: `dynamic-pwa-chat-${CACHE_VERSION.DYNAMIC}.${CACHE_VERSION.MINOR}`
};
const STATIC_RESOURCE_LIST = [
- '/',
+ './',
'/index.html',
- '/manifest.webmanifest',
+ '/manifest.json',
'/pages/chat.html',
+ '/pages/login.html',
'/pages/404.html',
- '/components/message.html',
'/main.css',
'/main.js'
];
-const SYNC_KEYWORDS = {
- POST_NEW_MESSAGES: 'post-new-messages',
- FETCH_NEW_MESSAGES: 'fetch-messages'
-};
-
-self.addEventListener('install', e => {
+self.addEventListener('install', (e: any) => {
console.log(
- '%c[ServiceWorker] installEvent fired',
+ '%c[ServiceWorker] installEvent fired\n',
+ e,
'background: #F7C8E0; color: #000'
);
- (e as any).waitUntil(
+
+ e.waitUntil(
caches.open(CACHE_LIST.STATIC_CACHE).then(cache => {
- cache.addAll(STATIC_RESOURCE_LIST);
+ console.log('[ServiceWorker] caching static resources');
+ return cache.addAll(STATIC_RESOURCE_LIST);
})
);
+
(self as any).skipWaiting();
});
-self.addEventListener('activate', e => {
+self.addEventListener('activate', (e: any) => {
console.log(
- '%c[ServiceWorker] activateEvent fired',
+ '%c[ServiceWorker] activateEvent fired\n',
+ e,
'background: #F7C8E0; color: #000'
);
- (self as any).clients.claim();
- (e as any).waitUntil(
- caches.keys().then(cacheNameList => {
+
+ e.waitUntil(
+ caches.keys().then(keys => {
return Promise.all(
- cacheNameList.map(cacheName => {
- if (
- cacheName !== CACHE_LIST.STATIC_CACHE &&
- cacheName !== CACHE_LIST.DYNAMIC_CACHE
- ) {
- console.log(`Deleting cache: ${cacheName}`);
- return caches.delete(cacheName);
- }
- })
+ keys
+ .filter(
+ key =>
+ key !== CACHE_LIST.STATIC_CACHE &&
+ key !== CACHE_LIST.DYNAMIC_CACHE
+ )
+ .map(key => caches.delete(key))
);
})
);
});
-self.addEventListener('fetch', e => {
+self.addEventListener('fetch', (e: any) => {
console.log(
- '%c[ServiceWorker] fetchEvent fired',
+ '%c[ServiceWorker] fetchEvent fired\n',
+ e,
'background: #F7C8E0; color: #000'
);
- if ((e as any).request.url.indexOf('www2.hs-esslingen.de') > -1) return;
- if ((e as any).request.url.indexOf('chrome-extension') > -1) return;
- if ((e as any).request.url.indexOf(location.host) == -1) return;
- if ((e as any).request.url.indexOf(location.pathname) == -1) return;
-
- if (
- STATIC_RESOURCE_LIST.join().indexOf(
- new URL((e as any).request.url).pathname
- ) > -1
- ) {
- (e as any).respondWith(
- cacheOnly_cachingStrategy(CACHE_LIST.STATIC_CACHE, (e as any).request.url)
- );
- } else {
- (e as any).respondWith(
- cacheFirstNetworkFallback_cachingStrategy(
- CACHE_LIST.DYNAMIC_CACHE,
- (e as any).request,
- (e as any).request.url
- )
- );
- }
-});
-
-function cacheOnly_cachingStrategy(cacheName, url) {
- return caches.open(cacheName).then(cache => {
- return cache.match(url);
- });
-}
-function cacheFirstNetworkFallback_cachingStrategy(cacheName, request, url) {
- return caches.match(request).then(cacheResponse => {
- return (
- cacheResponse ||
- fetch(request).then(fetchedResponse => {
- return caches.open(cacheName).then(dynamicCache => {
- dynamicCache.put(url, fetchedResponse.clone());
- return fetchedResponse;
- });
+ if (e.request.url.indexOf('www2.hs-esslingen.de') > -1) return;
+ if (e.request.url.indexOf('chrome-extension') > -1) return;
+
+ e.respondWith(
+ caches
+ .match(e.request.url)
+ .then(cacheResponse => {
+ return (
+ cacheResponse ||
+ fetch(e.request).then(networkResponse => {
+ return caches.open(CACHE_LIST.DYNAMIC_CACHE).then(cache => {
+ cache.put(e.request.url, networkResponse.clone());
+ return networkResponse;
+ });
+ })
+ );
+ })
+ .catch(() => {
+ if (e.request.url.indexOf('.html') > -1) {
+ return caches.match('/pages/404.html');
+ }
})
- );
- });
-}
-
-self.addEventListener('push', () => {
- console.log(
- '%c[ServiceWorker] pushEvent fired',
- 'background: #F7C8E0; color: #000'
- );
-});
-
-self.addEventListener('sync', e => {
- console.log(
- '%c[ServiceWorker] syncEvent fired:',
- 'background: #F7C8E0; color: #000'
- );
- if ((e as any).tag == SYNC_KEYWORDS.POST_NEW_MESSAGES) {
- (e as any).waitUntil(syncUnsentMessagesWithServer());
- }
-});
-
-function syncUnsentMessagesWithServer() {
- IndexedDBManager.getInstance()
- .then(db => {
- db.getUnsentMessages().then(data => {
- const requests = (data as any).map(message => {
- _apiService.sendMessage(message.token, message.text).then(() => {
- db.deleteUnsentMessage(message.id);
- });
- });
-
- return Promise.all(requests);
- });
- })
- .then(() => {
- sendMessageToAllClients({
- type: SYNC_KEYWORDS.FETCH_NEW_MESSAGES
- });
- });
-}
-
-function sendMessageToAllClients(message) {
- return (self as any).clients.matchAll().then(clients => {
- clients.forEach(client => {
- client.postMessage(message);
- });
- });
-}
-
-self.addEventListener('periodicsync', () => {
- console.log(
- '%c[ServiceWorker] periodicSyncEvent fired',
- 'background: #F7C8E0; color: #000'
);
});
diff --git a/src/ts/_interface/index.ts b/src/ts/_interface/index.ts
new file mode 100644
index 0000000..b41cfbb
--- /dev/null
+++ b/src/ts/_interface/index.ts
@@ -0,0 +1,8 @@
+export { default as Message } from './message/message.interface';
+export { default as SendMessage } from './message/sendMessage.interface';
+export { default as Response } from './response/response.interface';
+export { default as Route } from './route/route.interface';
+export { default as DeregisterUser } from './user/deregisterUser.interface';
+export { default as LoginUser } from './user/loginUser.interface';
+export { default as RegisterUser } from './user/registerUser.interface';
+export { default as User } from './user/user.interface';
diff --git a/src/ts/_interface/message.interface.ts b/src/ts/_interface/message/createMessage.interface.ts
similarity index 58%
rename from src/ts/_interface/message.interface.ts
rename to src/ts/_interface/message/createMessage.interface.ts
index e23e77e..a57c6ea 100644
--- a/src/ts/_interface/message.interface.ts
+++ b/src/ts/_interface/message/createMessage.interface.ts
@@ -1,8 +1,9 @@
-export interface message {
+export interface CreateMessage {
id: number;
+ chatid: number;
userhash: string;
usernickname: string;
- text: string;
+ text?: string;
+ photo?: Blob;
time: string;
- chatid: number;
}
diff --git a/src/ts/_interface/message/message.interface.ts b/src/ts/_interface/message/message.interface.ts
new file mode 100644
index 0000000..35d1ae9
--- /dev/null
+++ b/src/ts/_interface/message/message.interface.ts
@@ -0,0 +1,9 @@
+export default interface Message {
+ id: number;
+ chatid: number;
+ userhash: string;
+ usernickname: string;
+ text?: string;
+ photoid?: string;
+ time: string;
+}
diff --git a/src/ts/_interface/message/sendMessage.interface.ts b/src/ts/_interface/message/sendMessage.interface.ts
new file mode 100644
index 0000000..659cd9c
--- /dev/null
+++ b/src/ts/_interface/message/sendMessage.interface.ts
@@ -0,0 +1,4 @@
+export default interface SendMessage {
+ text?: string;
+ photo?: string;
+}
diff --git a/src/ts/_interface/response.interface.ts b/src/ts/_interface/response.interface.ts
deleted file mode 100644
index 94871d4..0000000
--- a/src/ts/_interface/response.interface.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { message } from './message.interface';
-
-export interface response {
- code: number;
- message: string;
- status: string;
- token?: string;
- hash?: string;
- messages?: Array
;
-}
diff --git a/src/ts/_interface/response/response.interface.ts b/src/ts/_interface/response/response.interface.ts
new file mode 100644
index 0000000..d780249
--- /dev/null
+++ b/src/ts/_interface/response/response.interface.ts
@@ -0,0 +1,10 @@
+import Message from '../message/message.interface';
+
+export default interface Response {
+ code: number;
+ message: string;
+ status: string;
+ token?: string;
+ hash?: string;
+ messages?: Message[];
+}
diff --git a/src/ts/_interface/route.interface.ts b/src/ts/_interface/route/route.interface.ts
similarity index 55%
rename from src/ts/_interface/route.interface.ts
rename to src/ts/_interface/route/route.interface.ts
index b9bc552..02e053e 100644
--- a/src/ts/_interface/route.interface.ts
+++ b/src/ts/_interface/route/route.interface.ts
@@ -1,6 +1,6 @@
-export interface route {
+export default interface Route {
+ id: number;
path: string;
title: string;
- description: string;
loginRequired: boolean;
}
diff --git a/src/ts/_interface/settings/setting.interface.ts b/src/ts/_interface/settings/setting.interface.ts
new file mode 100644
index 0000000..d5422f0
--- /dev/null
+++ b/src/ts/_interface/settings/setting.interface.ts
@@ -0,0 +1,8 @@
+import { FontSizeOption, ThemeOption, WallpaperOption } from './settings';
+
+export interface Setting {
+ id?: number;
+ theme: ThemeOption;
+ wallpaper: WallpaperOption;
+ fontSize: FontSizeOption;
+}
diff --git a/src/ts/_interface/settings/settings/fontSize.interface.ts b/src/ts/_interface/settings/settings/fontSize.interface.ts
new file mode 100644
index 0000000..c1aa07d
--- /dev/null
+++ b/src/ts/_interface/settings/settings/fontSize.interface.ts
@@ -0,0 +1,3 @@
+export default interface FontSizeOption {
+ current: 's' | 'm' | 'l';
+}
diff --git a/src/ts/_interface/settings/settings/index.ts b/src/ts/_interface/settings/settings/index.ts
new file mode 100644
index 0000000..b2b7fa4
--- /dev/null
+++ b/src/ts/_interface/settings/settings/index.ts
@@ -0,0 +1,3 @@
+export { default as FontSizeOption } from './fontSize.interface';
+export { default as ThemeOption } from './theme.interface';
+export { default as WallpaperOption } from './wallpaper.interface';
diff --git a/src/ts/_interface/settings/settings/theme.interface.ts b/src/ts/_interface/settings/settings/theme.interface.ts
new file mode 100644
index 0000000..9a34060
--- /dev/null
+++ b/src/ts/_interface/settings/settings/theme.interface.ts
@@ -0,0 +1,4 @@
+export default interface ThemeOption {
+ design: 'light' | 'dark' | null;
+ accent: 'green' | 'purple';
+}
diff --git a/src/ts/_interface/settings/settings/wallpaper.interface.ts b/src/ts/_interface/settings/settings/wallpaper.interface.ts
new file mode 100644
index 0000000..ad6f9ac
--- /dev/null
+++ b/src/ts/_interface/settings/settings/wallpaper.interface.ts
@@ -0,0 +1,3 @@
+export default interface WallpaperOption {
+ current: 1 | 2 | 3 | null;
+}
diff --git a/src/ts/_interface/user.interface.ts b/src/ts/_interface/user.interface.ts
deleted file mode 100644
index 3fd33fa..0000000
--- a/src/ts/_interface/user.interface.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export interface user {
- userid: string;
- password: string;
- nickname?: string;
- fullname?: string;
-}
diff --git a/src/ts/_interface/user/deregisterUser.interface.ts b/src/ts/_interface/user/deregisterUser.interface.ts
new file mode 100644
index 0000000..18444f0
--- /dev/null
+++ b/src/ts/_interface/user/deregisterUser.interface.ts
@@ -0,0 +1,5 @@
+export default interface DeregisterUser {
+ userid: string;
+ password: string;
+ token: string;
+}
diff --git a/src/ts/_interface/user/loginUser.interface.ts b/src/ts/_interface/user/loginUser.interface.ts
new file mode 100644
index 0000000..69d1d35
--- /dev/null
+++ b/src/ts/_interface/user/loginUser.interface.ts
@@ -0,0 +1,4 @@
+export default interface loginUser {
+ userid: string;
+ password: string;
+}
diff --git a/src/ts/_interface/user/registerUser.interface.ts b/src/ts/_interface/user/registerUser.interface.ts
new file mode 100644
index 0000000..8098430
--- /dev/null
+++ b/src/ts/_interface/user/registerUser.interface.ts
@@ -0,0 +1,6 @@
+export default interface RegisterUser {
+ userid: string;
+ password: string;
+ nickname: string;
+ fullname: string;
+}
diff --git a/src/ts/_interface/user/user.interface.ts b/src/ts/_interface/user/user.interface.ts
new file mode 100644
index 0000000..f09df88
--- /dev/null
+++ b/src/ts/_interface/user/user.interface.ts
@@ -0,0 +1,4 @@
+export default interface User {
+ token: string;
+ hash: string;
+}
diff --git a/src/ts/_interface/user/userCookie.interface.ts b/src/ts/_interface/user/userCookie.interface.ts
new file mode 100644
index 0000000..b527290
--- /dev/null
+++ b/src/ts/_interface/user/userCookie.interface.ts
@@ -0,0 +1,5 @@
+export default interface UserCookie {
+ token: string;
+ hash: string;
+ userid: string;
+}
diff --git a/src/ts/_interface/wallpaper/wallpaper.interface.ts b/src/ts/_interface/wallpaper/wallpaper.interface.ts
new file mode 100644
index 0000000..5640e7f
--- /dev/null
+++ b/src/ts/_interface/wallpaper/wallpaper.interface.ts
@@ -0,0 +1,4 @@
+export interface StoredWallpaper {
+ id: number;
+ image: Blob;
+}
diff --git a/src/ts/_service/api.service.ts b/src/ts/_service/api.service.ts
deleted file mode 100644
index 9615324..0000000
--- a/src/ts/_service/api.service.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import { response } from '../_interface/response.interface';
-import { user } from '../_interface/user.interface';
-
-export class ApiService {
- private url: string = 'https://www2.hs-esslingen.de/~melcher/map/chat/api/';
-
- private async httpRequest(
- data?: URLSearchParams | string,
- method: string = 'POST'
- ): Promise {
- const response = await fetch(this.url, {
- method: method,
- headers: {
- 'Content-Type': 'application/json'
- },
- body: data
- });
-
- if (!response.ok) {
- throw response;
- }
-
- return await response.json();
- }
-
- constructor() {}
-
- public async registerUser(user: user): Promise {
- const data = JSON.stringify({
- request: 'register',
- userid: user.userid,
- password: user.password,
- nickname: user.nickname,
- fullname: user.fullname
- });
-
- try {
- return await this.httpRequest(data);
- } catch (error: unknown) {
- throw error;
- }
- }
-
- public async deregisterUser(token: string): Promise {
- const data = JSON.stringify({
- request: 'deregister',
- token: token
- });
-
- try {
- return await this.httpRequest(data);
- } catch (error: unknown) {
- console.error(error.toString());
- throw error;
- }
- }
-
- public async logInUser(user: user): Promise {
- const data = JSON.stringify({
- request: 'login',
- userid: user.userid,
- password: user.password
- });
-
- try {
- return await this.httpRequest(data);
- } catch (error: unknown) {
- throw error;
- }
- }
-
- public async logOutUser(token: string): Promise {
- const data = JSON.stringify({
- request: 'logout',
- token: token
- });
-
- try {
- return await this.httpRequest(data);
- } catch (error: unknown) {
- console.error(error.toString());
- throw error;
- }
- }
-
- public sendMessage(token: string, message: string): Promise {
- const data = JSON.stringify({
- request: 'sendmessage',
- token: token,
- text: message
- });
-
- try {
- return this.httpRequest(data);
- } catch (error: unknown) {
- console.error(error.toString());
- throw error;
- }
- }
-
- public fetchMessages(token: string): Promise {
- const data = JSON.stringify({
- request: 'fetchmessages',
- token: token
- });
-
- try {
- return this.httpRequest(data);
- } catch (error: unknown) {
- console.error(error.toString());
- throw error;
- }
- }
-}
diff --git a/src/ts/_service/cookie.service.ts b/src/ts/_service/cookie/cookie.ts
similarity index 68%
rename from src/ts/_service/cookie.service.ts
rename to src/ts/_service/cookie/cookie.ts
index d30b7a3..8e041b2 100644
--- a/src/ts/_service/cookie.service.ts
+++ b/src/ts/_service/cookie/cookie.ts
@@ -1,7 +1,19 @@
export class CookieService {
- constructor() {}
+ private static instance: CookieService;
- new(key: string, value: string, expirationDays: number = 0): void {
+ private constructor() {
+ console.log('[Cookie] - instance created');
+ }
+
+ public static getInstance(): CookieService {
+ if (!CookieService.instance) {
+ CookieService.instance = new CookieService();
+ }
+
+ return this.instance;
+ }
+
+ create(key: string, value: string, expirationDays: number = 0): void {
let cookie = `${key}=${value}`;
if (expirationDays > 0) {
let date = new Date();
@@ -20,12 +32,13 @@ export class CookieService {
response = cookie.split('=')[1];
}
});
+
return response;
}
update(key: string, value: string, expirationDays: number = 0): boolean {
if (this.get(key)) {
- this.new(key, value, expirationDays);
+ this.create(key, value, expirationDays);
return true;
}
return false;
diff --git a/src/ts/_service/http/endpoints/deregister.ts b/src/ts/_service/http/endpoints/deregister.ts
new file mode 100644
index 0000000..795cdb8
--- /dev/null
+++ b/src/ts/_service/http/endpoints/deregister.ts
@@ -0,0 +1,27 @@
+import { DeregisterUser, Response } from '../../../_interface';
+import { Http } from '../http';
+
+/**
+ * Deletes the User from the Server
+ * @param user
+ */
+export default async function deregister(
+ user: DeregisterUser
+): Promise {
+ const http = Http.getInstance();
+
+ let options: { [key: string]: any } = {
+ request: 'deregister',
+ ...user
+ };
+
+ return await http
+ .fetchJson('POST', options)
+ .then(response => {
+ return response;
+ })
+ .catch(error => {
+ console.log('[Http]:register - Something went Wrong\n', error);
+ return null;
+ });
+}
diff --git a/src/ts/_service/http/endpoints/fetchmessages.ts b/src/ts/_service/http/endpoints/fetchmessages.ts
new file mode 100644
index 0000000..7943f41
--- /dev/null
+++ b/src/ts/_service/http/endpoints/fetchmessages.ts
@@ -0,0 +1,39 @@
+import { Message } from '../../../_interface';
+import { Auth } from '../../../auth/auth';
+import { Http } from '../http';
+
+/**
+ * Fetches the Messages from the Server
+ * If from parameter is provided, it will fetch all messages after the provided id
+ * @param from
+ */
+export default async function fetchMessages(
+ from?: number
+): Promise {
+ const http = Http.getInstance();
+ const auth = Auth.getInstance();
+
+ if (auth.getActiveUser() === null) {
+ console.log('[Http]:fetchMessages - No User is logged in');
+ return null;
+ }
+
+ let options: { [key: string]: any } = {
+ request: 'fetchmessages',
+ token: auth.getActiveUser()?.token
+ };
+
+ if (from !== undefined) {
+ options.from = from;
+ }
+
+ return await http
+ .fetchJson('POST', options)
+ .then(response => {
+ return response.messages as Message[];
+ })
+ .catch(error => {
+ console.log('[Http]:fetchMessages - Something went Wrong\n', error);
+ return null;
+ });
+}
diff --git a/src/ts/_service/http/endpoints/fetchphoto.ts b/src/ts/_service/http/endpoints/fetchphoto.ts
new file mode 100644
index 0000000..8e459c2
--- /dev/null
+++ b/src/ts/_service/http/endpoints/fetchphoto.ts
@@ -0,0 +1,35 @@
+import { Response } from '../../../_interface';
+import { Auth } from '../../../auth/auth';
+import { Http } from '../http';
+
+/**
+ * Fetches the Image with the provided ID
+ * @param photoid
+ */
+export default async function fetchPhoto(
+ photoid: string
+): Promise {
+ const http = Http.getInstance();
+ const auth = Auth.getInstance();
+
+ if (auth.getActiveUser() === null) {
+ console.log('[Http]:fetchPhoto - No User is logged in');
+ return null;
+ }
+
+ let options: { [key: string]: any } = {
+ request: 'fetchphoto',
+ token: auth.getActiveUser()?.token,
+ photoid: photoid
+ };
+
+ return await http
+ .fetchBlob('POST', options)
+ .then(response => {
+ return response;
+ })
+ .catch(error => {
+ console.log('[Http]:fetchPhoto - Something went Wrong\n', error);
+ return null;
+ });
+}
diff --git a/src/ts/_service/http/endpoints/login.ts b/src/ts/_service/http/endpoints/login.ts
new file mode 100644
index 0000000..8ab684b
--- /dev/null
+++ b/src/ts/_service/http/endpoints/login.ts
@@ -0,0 +1,35 @@
+import { LoginUser, User } from '../../../_interface';
+import { Auth } from '../../../auth/auth';
+import { Http } from '../http';
+
+/**
+ * Logs the User in
+ * @param user
+ */
+export default async function login(user: LoginUser): Promise {
+ const http = Http.getInstance();
+ const auth = Auth.getInstance();
+
+ if (auth.getActiveUser() !== null) {
+ console.log('[Http]:login - An User is already logged in');
+ return null;
+ }
+
+ let options: { [key: string]: any } = {
+ request: 'login',
+ ...user
+ };
+
+ return await http
+ .fetchJson('POST', options)
+ .then(response => {
+ return {
+ token: response.token as string,
+ hash: response.hash as string
+ };
+ })
+ .catch(error => {
+ console.log('[Http]:login - Something went Wrong\n', error);
+ return null;
+ });
+}
diff --git a/src/ts/_service/http/endpoints/logout.ts b/src/ts/_service/http/endpoints/logout.ts
new file mode 100644
index 0000000..c181aeb
--- /dev/null
+++ b/src/ts/_service/http/endpoints/logout.ts
@@ -0,0 +1,31 @@
+import { Response } from '../../../_interface';
+import { Auth } from '../../../auth/auth';
+import { Http } from '../http';
+
+/**
+ * Logs the User out
+ */
+export default async function logout(): Promise {
+ const http = Http.getInstance();
+ const auth = Auth.getInstance();
+
+ if (auth.getActiveUser() === null) {
+ console.log('[Http]:logout - No User is logged in');
+ return null;
+ }
+
+ let options: { [key: string]: any } = {
+ request: 'logout',
+ toke: auth.getActiveUser()?.token
+ };
+
+ return await http
+ .fetchJson('POST', options)
+ .then(response => {
+ return response;
+ })
+ .catch(error => {
+ console.log('[Http]:logout - Something went Wrong\n', error);
+ return null;
+ });
+}
diff --git a/src/ts/_service/http/endpoints/register.ts b/src/ts/_service/http/endpoints/register.ts
new file mode 100644
index 0000000..79a06c5
--- /dev/null
+++ b/src/ts/_service/http/endpoints/register.ts
@@ -0,0 +1,30 @@
+import { RegisterUser, User } from '../../../_interface';
+import { Http } from '../http';
+
+/**
+ * Registers a new User with the Provided Information
+ * @param newUser
+ */
+export default async function register(
+ newUser: RegisterUser
+): Promise {
+ const http = Http.getInstance();
+
+ let options: { [key: string]: any } = {
+ request: 'register',
+ ...newUser
+ };
+
+ return await http
+ .fetchJson('POST', options)
+ .then(response => {
+ return {
+ token: response.token as string,
+ hash: response.hash as string
+ };
+ })
+ .catch(error => {
+ console.log('[Http]:register - Something went Wrong\n', error);
+ return null;
+ });
+}
diff --git a/src/ts/_service/http/endpoints/sendmessage.ts b/src/ts/_service/http/endpoints/sendmessage.ts
new file mode 100644
index 0000000..f5293bf
--- /dev/null
+++ b/src/ts/_service/http/endpoints/sendmessage.ts
@@ -0,0 +1,42 @@
+import { Response, SendMessage } from '../../../_interface';
+import { Auth } from '../../../auth/auth';
+import { Http } from '../http';
+
+/**
+ * Sends a Message containing text and/or a photo to the server
+ * @param text
+ * @param photo
+ * @returns
+ */
+export default async function sendMessage(
+ message: SendMessage
+): Promise {
+ const http = Http.getInstance();
+ const auth = Auth.getInstance();
+
+ if (auth.getActiveUser() === null) {
+ console.log('[Http]:sendMessage - No User is logged in');
+ return null;
+ }
+
+ if (message.text === undefined && message.photo === undefined) {
+ console.log('[Http]:sendMessage - Message is empty');
+ return null;
+ }
+
+ let body: { [key: string]: any } = {
+ request: 'sendmessage',
+ token: auth.getActiveUser()?.token,
+ ...message
+ };
+
+ return await http
+ .fetchJson('POST', body)
+ .then(response => {
+ return response;
+ })
+ .catch(error => {
+ console.log('[Http]:sendMessage - Something went Wrong\n', error);
+ return null;
+ });
+}
diff --git a/src/ts/_service/http/http.ts b/src/ts/_service/http/http.ts
new file mode 100644
index 0000000..223d94c
--- /dev/null
+++ b/src/ts/_service/http/http.ts
@@ -0,0 +1,52 @@
+import { Response } from '../../_interface';
+import { CONFIG } from '../../common';
+
+export class Http {
+ private static instance: Http;
+ private readonly baseUrl: string =
+ CONFIG.BASE_URL_API || 'localhost:8081/api/v1';
+
+ private constructor() {
+ console.log('[Http] - instance created');
+ }
+
+ public static getInstance(): Http {
+ if (!Http.instance) {
+ Http.instance = new Http();
+ }
+
+ return this.instance;
+ }
+
+ public async fetchJson(method: string, body: Object): Promise {
+ const response = await fetch(this.baseUrl, {
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(body)
+ });
+
+ if (!response.ok) {
+ throw response;
+ }
+
+ return await response.json();
+ }
+
+ public async fetchBlob(method: string, body: Object) {
+ const response = await fetch(this.baseUrl, {
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(body)
+ });
+
+ if (!response.ok) {
+ throw response;
+ }
+
+ return await response.blob();
+ }
+}
diff --git a/src/ts/_service/http/index.ts b/src/ts/_service/http/index.ts
new file mode 100644
index 0000000..9c8c919
--- /dev/null
+++ b/src/ts/_service/http/index.ts
@@ -0,0 +1,7 @@
+export { default as deregister } from './endpoints/deregister';
+export { default as fetchMessages } from './endpoints/fetchmessages';
+export { default as fetchPhoto } from './endpoints/fetchphoto';
+export { default as login } from './endpoints/login';
+export { default as logout } from './endpoints/logout';
+export { default as register } from './endpoints/register';
+export { default as sendMessage } from './endpoints/sendmessage';
diff --git a/src/ts/_service/idb/idb.ts b/src/ts/_service/idb/idb.ts
new file mode 100644
index 0000000..74e5b3c
--- /dev/null
+++ b/src/ts/_service/idb/idb.ts
@@ -0,0 +1,277 @@
+import { Message, SendMessage } from '../../_interface';
+import { CreateMessage } from '../../_interface/message/createMessage.interface';
+import { Setting } from '../../_interface/settings/setting.interface';
+import { StoredWallpaper } from '../../_interface/wallpaper/wallpaper.interface';
+
+export class IndexedDBManager {
+ private static instance: IndexedDBManager;
+
+ private db!: IDBDatabase;
+ private dbName: string = 'pwa-chat';
+ private dbVersion: number = 1;
+ private selected: { name: string; options: Object } = {
+ name: '',
+ options: {}
+ };
+ private dbOptions: { name: string; options: Object }[] = [
+ {
+ name: 'messages',
+ options: {
+ keyPath: 'id'
+ }
+ },
+ {
+ name: 'unsentMessages',
+ options: {
+ keyPath: 'id',
+ autoIncrement: true
+ }
+ },
+ {
+ name: 'wallpaper',
+ options: {
+ keyPath: 'id'
+ }
+ },
+ {
+ name: 'settings',
+ options: {
+ keyPath: 'id',
+ autoIncrement: true
+ }
+ }
+ ];
+
+ private constructor() {
+ console.log('[IndexedDBManager] - instance created');
+ }
+
+ public static getInstance(): IndexedDBManager {
+ if (!IndexedDBManager.instance) {
+ IndexedDBManager.instance = new IndexedDBManager();
+ }
+ return IndexedDBManager.instance;
+ }
+
+ public async init(): Promise {
+ return new Promise((resolve, reject) => {
+ const request: IDBOpenDBRequest = indexedDB.open(
+ this.dbName,
+ this.dbVersion
+ );
+
+ request.onerror = (e: Event) => {
+ console.error('[IndexedDBManager] Initialization error', e);
+ reject(e);
+ };
+
+ request.onupgradeneeded = () => {
+ console.log('[IndexedDBManager] needed Upgrade');
+
+ this.db = request.result;
+ this.dbOptions.forEach((option: { name: string; options: Object }) => {
+ this.db.createObjectStore(option.name, option.options);
+ });
+ };
+
+ request.onsuccess = (e: Event) => {
+ console.log('[IndexedDBManager] Initialized');
+
+ this.db = request.result;
+ resolve();
+ };
+ });
+ }
+
+ public messages() {
+ this.selected = this.dbOptions[0];
+ return this;
+ }
+
+ public unsentMessages() {
+ this.selected = this.dbOptions[1];
+ return this;
+ }
+
+ public wallpaper() {
+ this.selected = this.dbOptions[2];
+ return this;
+ }
+
+ public settings() {
+ this.selected = this.dbOptions[3];
+ return this;
+ }
+
+ public get(id: number): Promise {
+ return new Promise((resolve, reject) => {
+ const transaction: IDBTransaction = this.db.transaction(
+ this.selected.name,
+ 'readonly'
+ );
+ const objectStore: IDBObjectStore = transaction.objectStore(
+ this.selected.name
+ );
+ const request: IDBRequest = objectStore.get(id);
+
+ request.onerror = (e: Event) => {
+ console.error(
+ '[IndexedDBManager] Get ' + this.selected.name + ' error',
+ e
+ );
+ reject(e);
+ };
+
+ request.onsuccess = () => {
+ console.log(
+ '[IndexedDBManager] Get ' + this.selected.name + ' success'
+ );
+ resolve(request.result);
+ };
+ });
+ }
+
+ public getAll(): Promise {
+ return new Promise((resolve, reject) => {
+ const transaction: IDBTransaction = this.db.transaction(
+ this.selected.name,
+ 'readonly'
+ );
+ const objectStore: IDBObjectStore = transaction.objectStore(
+ this.selected.name
+ );
+ const request: IDBRequest = objectStore.getAll();
+
+ request.onerror = (e: Event) => {
+ console.error(
+ '[IndexedDBManager] Get all ' + this.selected.name + ' error',
+ e
+ );
+ reject(e);
+ };
+
+ request.onsuccess = () => {
+ console.log(
+ '[IndexedDBManager] Get all ' + this.selected.name + ' success'
+ );
+ resolve(request.result);
+ };
+ });
+ }
+
+ public add(
+ data: CreateMessage | SendMessage | Setting | StoredWallpaper
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ const transaction: IDBTransaction = this.db.transaction(
+ this.selected.name,
+ 'readwrite'
+ );
+ const objectStore: IDBObjectStore = transaction.objectStore(
+ this.selected.name
+ );
+ const request: IDBRequest = objectStore.add(data);
+
+ request.onerror = (e: Event) => {
+ console.error(
+ '[IndexedDBManager] Add ' + this.selected.name + ' error',
+ e
+ );
+ reject(e);
+ };
+
+ request.onsuccess = () => {
+ console.log(
+ '[IndexedDBManager] Add ' + this.selected.name + ' success'
+ );
+ resolve(true);
+ };
+ });
+ }
+
+ public update(
+ data: Message | SendMessage | Setting | StoredWallpaper
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ const transaction: IDBTransaction = this.db.transaction(
+ this.selected.name,
+ 'readwrite'
+ );
+ const objectStore: IDBObjectStore = transaction.objectStore(
+ this.selected.name
+ );
+ const request: IDBRequest = objectStore.put(data);
+
+ request.onerror = (e: Event) => {
+ console.error(
+ '[IndexedDBManager] Update ' + this.selected.name + ' error',
+ e
+ );
+ reject(e);
+ };
+
+ request.onsuccess = () => {
+ console.log(
+ '[IndexedDBManager] Update ' + this.selected.name + ' success'
+ );
+ resolve(true);
+ };
+ });
+ }
+
+ public delete(id: number): Promise {
+ return new Promise((resolve, reject) => {
+ const transaction: IDBTransaction = this.db.transaction(
+ this.selected.name,
+ 'readwrite'
+ );
+ const objectStore: IDBObjectStore = transaction.objectStore(
+ this.selected.name
+ );
+ const request: IDBRequest = objectStore.delete(id);
+
+ request.onerror = (e: Event) => {
+ console.error(
+ '[IndexedDBManager] Delete ' + this.selected.name + ' error',
+ e
+ );
+ reject(e);
+ };
+
+ request.onsuccess = () => {
+ console.log(
+ '[IndexedDBManager] Delete ' + this.selected.name + ' success'
+ );
+ resolve(true);
+ };
+ });
+ }
+
+ public clear() {
+ return new Promise((resolve, reject) => {
+ const transaction: IDBTransaction = this.db.transaction(
+ this.selected.name,
+ 'readwrite'
+ );
+ const objectStore: IDBObjectStore = transaction.objectStore(
+ this.selected.name
+ );
+ const request: IDBRequest = objectStore.clear();
+
+ request.onerror = (e: Event) => {
+ console.error(
+ '[IndexedDBManager] Clear ' + this.selected.name + ' error',
+ e
+ );
+ reject(e);
+ };
+
+ request.onsuccess = () => {
+ console.log(
+ '[IndexedDBManager] Clear ' + this.selected.name + ' success'
+ );
+ resolve(true);
+ };
+ });
+ }
+}
diff --git a/src/ts/_service/storage.service.ts b/src/ts/_service/storage.service.ts
deleted file mode 100644
index d44b75f..0000000
--- a/src/ts/_service/storage.service.ts
+++ /dev/null
@@ -1,264 +0,0 @@
-import { message } from '../_interface/message.interface';
-
-export class IndexedDBManager {
- private db: IDBDatabase = null;
- private dbVersion: number = 1;
- private dbName: string = 'pwa-chat';
- private dbStoreName: Array = ['messages', 'unsent_messages'];
- private static _instance: IndexedDBManager;
-
- private constructor() {}
-
- public static async getInstance(): Promise {
- if (!IndexedDBManager._instance) {
- IndexedDBManager._instance = new IndexedDBManager();
- await IndexedDBManager._instance.init();
- }
- return IndexedDBManager._instance;
- }
-
- private async init(): Promise {
- return new Promise((resolve, reject) => {
- const request: IDBOpenDBRequest = indexedDB.open(
- this.dbName,
- this.dbVersion
- );
- request.onerror = (e: Event) => {
- console.error('[IndexedDB] Initialization error', e);
- reject(e);
- };
-
- request.onupgradeneeded = (e: Event) => {
- console.log('[IndexedDB] needed Upgrade');
-
- this.db = request.result;
- this.db.createObjectStore(this.dbStoreName[0], {
- keyPath: 'id'
- });
- this.db.createObjectStore(this.dbStoreName[1], {
- keyPath: 'id',
- autoIncrement: true
- });
- };
-
- request.onsuccess = (e: Event) => {
- console.log('[IndexedDB] Initialized');
-
- this.db = request.result;
- resolve();
- };
- });
- }
-
- // Messages
- public async addMessage(data: message): Promise {
- return new Promise((resolve, reject) => {
- const transaction: IDBTransaction = this.db.transaction(
- this.dbStoreName[0],
- 'readwrite'
- );
- const objectStore: IDBObjectStore = transaction.objectStore(
- this.dbStoreName[0]
- );
- const request: IDBRequest = objectStore.add({ ...data });
-
- request.onerror = e => {
- reject(e);
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
- });
- }
-
- public async getMessage(id: number): Promise {
- return new Promise((resolve, reject) => {
- const transaction: IDBTransaction = this.db.transaction(
- this.dbStoreName[0],
- 'readonly'
- );
- const objectStore: IDBObjectStore = transaction.objectStore(
- this.dbStoreName[0]
- );
- const request: IDBRequest = objectStore.get(id);
-
- request.onerror = e => {
- reject(e);
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
- });
- }
-
- public async getAllMessages(): Promise {
- return new Promise((resolve, reject) => {
- const transaction: IDBTransaction = this.db.transaction(
- this.dbStoreName[0],
- 'readonly'
- );
- const objectStore: IDBObjectStore = transaction.objectStore(
- this.dbStoreName[0]
- );
- const request: IDBRequest = objectStore.getAll();
-
- request.onerror = e => {
- reject(e);
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
- });
- }
-
- public async deleteMessage(id: number): Promise {
- return new Promise((resolve, reject) => {
- const transaction: IDBTransaction = this.db.transaction(
- this.dbStoreName[0],
- 'readwrite'
- );
- const objectStore: IDBObjectStore = transaction.objectStore(
- this.dbStoreName[0]
- );
- const request: IDBRequest = objectStore.delete(id);
-
- request.onerror = e => {
- reject(e);
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
- });
- }
-
- public async deleteAllMessages(): Promise {
- return new Promise((resolve, reject) => {
- const transaction: IDBTransaction = this.db.transaction(
- this.dbStoreName[0],
- 'readwrite'
- );
- const objectStore: IDBObjectStore = transaction.objectStore(
- this.dbStoreName[0]
- );
- const request: IDBRequest = objectStore.clear();
-
- request.onerror = e => {
- reject(e);
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
- });
- }
-
- // Unsent Messages
- public async addUnsentMessage(data: Object): Promise {
- return new Promise((resolve, reject) => {
- const transaction: IDBTransaction = this.db.transaction(
- this.dbStoreName[1],
- 'readwrite'
- );
- const objectStore: IDBObjectStore = transaction.objectStore(
- this.dbStoreName[1]
- );
- const request: IDBRequest = objectStore.add({ ...data });
-
- request.onerror = e => {
- reject(e);
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
- });
- }
-
- public async getUnsentMessages(): Promise {
- return new Promise((resolve, reject) => {
- const transaction: IDBTransaction = this.db.transaction(
- this.dbStoreName[1],
- 'readonly'
- );
- const objectStore: IDBObjectStore = transaction.objectStore(
- this.dbStoreName[1]
- );
- const request: IDBRequest = objectStore.getAll();
-
- request.onerror = e => {
- reject(e);
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
- });
- }
-
- public async getAllUnsentMessages(): Promise {
- return new Promise((resolve, reject) => {
- const transaction: IDBTransaction = this.db.transaction(
- this.dbStoreName[1],
- 'readonly'
- );
- const objectStore: IDBObjectStore = transaction.objectStore(
- this.dbStoreName[1]
- );
- const request: IDBRequest = objectStore.getAll();
-
- request.onerror = e => {
- reject(e);
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
- });
- }
-
- public async deleteUnsentMessage(id: number): Promise {
- return new Promise((resolve, reject) => {
- const transaction: IDBTransaction = this.db.transaction(
- this.dbStoreName[1],
- 'readwrite'
- );
- const objectStore: IDBObjectStore = transaction.objectStore(
- this.dbStoreName[1]
- );
- const request: IDBRequest = objectStore.delete(id);
-
- request.onerror = e => {
- reject(e);
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
- });
- }
-
- public async deleteAllUnsentMessages(): Promise {
- return new Promise((resolve, reject) => {
- const transaction: IDBTransaction = this.db.transaction(
- this.dbStoreName[1],
- 'readwrite'
- );
- const objectStore: IDBObjectStore = transaction.objectStore(
- this.dbStoreName[1]
- );
- const request: IDBRequest = objectStore.clear();
-
- request.onerror = e => {
- reject(e);
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
- });
- }
-}
diff --git a/src/ts/_validation/form.validator.ts b/src/ts/_validation/form.validator.ts
deleted file mode 100644
index 2c1c86b..0000000
--- a/src/ts/_validation/form.validator.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import { user } from '../_interface/user.interface';
-import { Auth } from '../auth';
-
-export class formValidator {
- private inputs: Object = {
- userid: {
- regex: /^[a-zA-Z0-9]{8}$/,
- errorCodes: [451, 454],
- errorMessage:
- 'Username must be 8 characters long and can only contain letters and numbers (HSE ID).'
- },
- password: {
- regex: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{6,}$/,
- errorCodes: [455],
- errorMessage:
- 'Password must contain at least 6 characters, including 1 number.'
- },
- passwordConfirm: {
- confirm: 'password',
- errorMessage: 'Passwords do not match.'
- },
- nickname: {
- regex: /^.{6,}$/,
- errorMessage: 'Nickname must be at least 6 characters long.'
- },
- fullname: {
- regex: /^.{3,}$/,
- errorMessage: 'Please provide a First and Last name.'
- }
- };
- private _auth = Auth.getInstance();
-
- constructor() {}
-
- public setup(form: HTMLElement, state) {
- const inputGroups: NodeListOf =
- form.querySelectorAll('.form__group');
-
- form.addEventListener('submit', e => {
- e.preventDefault();
-
- const form: HTMLFormElement = document.getElementById(
- 'form'
- ) as HTMLFormElement;
-
- for (let inputGroup of inputGroups) {
- this.validateInput(
- inputGroup,
- this.inputs[inputGroup.dataset.formField]
- );
- }
-
- if (form.querySelectorAll('.error').length > 0) return;
-
- const formData: FormData = new FormData(e.target as HTMLFormElement);
- let userObj: user = { userid: '', password: '' };
- for (let pair of formData.entries()) {
- userObj[pair[0]] = pair[1];
- }
-
- const stayLoggedIn: boolean = (
- (e.target as HTMLFormElement).querySelector(
- '#stayLoggedIn'
- ) as HTMLInputElement
- ).checked;
-
- switch (state) {
- case 'login':
- this._auth
- .login(userObj, stayLoggedIn)
- .then(() => {
- window.navigateTo('/');
- })
- .catch(err => {
- this.handleServerSideError(form, err);
- });
- break;
- case 'signup':
- this._auth
- .register(userObj, stayLoggedIn)
- .then(() => {
- window.navigateTo('/');
- })
- .catch(err => {
- this.handleServerSideError(form, err);
- });
- break;
- default:
- break;
- }
- });
-
- for (let inputGroup of inputGroups) {
- inputGroup.querySelector('input').addEventListener('blur', () => {
- this.validateInput(
- inputGroup,
- this.inputs[inputGroup.dataset.formField]
- );
- });
-
- if (inputGroup.dataset.formField.indexOf('password') > -1) {
- inputGroup
- .getElementsByClassName('form__group__icon')[0]
- .addEventListener('click', e => {
- (e.target as HTMLElement).classList.toggle('active');
- console.log('Test');
-
- const input = (e.target as HTMLElement).previousElementSibling;
- if (input.getAttribute('type') === 'password') {
- input.setAttribute('type', 'text');
- } else {
- input.setAttribute('type', 'password');
- }
- });
- }
- }
- }
-
- public validateInput(field: HTMLElement, settings: Object) {
- const inputElement: HTMLInputElement = field.querySelector('input');
- const inputValue: string = inputElement.value;
-
- if (settings['regex'] && inputValue.match(settings['regex'])) {
- this.setStatus(field, '');
- return;
- }
-
- if (settings['confirm']) {
- const passwordElement: HTMLInputElement = document.getElementById(
- settings['confirm']
- ) as HTMLInputElement;
- if (passwordElement.value === inputValue) {
- this.setStatus(field, '');
- return;
- }
- }
-
- this.setStatus(field, settings['errorMessage']);
- }
-
- public handleServerSideError(form: HTMLFormElement, err) {
- for (let key in this.inputs) {
- if (this.inputs[key].errorCodes?.includes(err.status)) {
- this.setStatus(
- form.querySelector(`[data-form-field="${key}"]`),
- err.statusText
- );
- return;
- }
- }
- alert(
- 'An unknown error has occurred.\nPlease try again later.\nerrorCode: ' +
- err.status +
- '\nerrorMessage: ' +
- err.statusText
- );
- }
-
- public setStatus(field: HTMLElement, message: string = '') {
- const errorElement: HTMLElement = field.querySelector('.error-message');
-
- if (message === '') {
- errorElement.innerHTML = '';
- if (field.classList.contains('error')) field.classList.remove('error');
- } else {
- errorElement.innerHTML = message;
- if (!field.classList.contains('error')) field.classList.add('error');
- }
- }
-}
diff --git a/src/ts/app.ts b/src/ts/app.ts
deleted file mode 100644
index 554b2d4..0000000
--- a/src/ts/app.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { formValidator } from './_validation/form.validator';
-import { Chat } from './chat';
-import { Router } from './router';
-
-export class App {
- private _router: Router;
- private validator: formValidator;
- private chat: Chat;
-
- private state: string;
-
- constructor() {
- this._router = new Router();
- this.validator = new formValidator();
- this.chat = new Chat();
-
- this.state = '';
-
- this.init();
- }
-
- public init() {
- document.body.addEventListener('spaContentLoaded', () => {
- this.updateState();
- });
- }
-
- private updateState() {
- const firstElement: HTMLElement | null = document.body
- .firstElementChild as HTMLElement;
- this.state = firstElement?.dataset.state;
-
- switch (this.state) {
- case 'login':
- this.validator.setup(firstElement.querySelector('.form'), this.state);
- break;
- case 'signup':
- this.validator.setup(firstElement.querySelector('.form'), this.state);
- break;
- case 'chat':
- this.chat.init();
- break;
- case '404':
- break;
- default:
- console.warn('Content did not load Correctly');
- this._router.navigateTo('/');
- break;
- }
- }
-}
diff --git a/src/ts/auth.ts b/src/ts/auth.ts
deleted file mode 100644
index b814247..0000000
--- a/src/ts/auth.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import { response } from './_interface/response.interface';
-import { user } from './_interface/user.interface';
-import { ApiService } from './_service/api.service';
-import { CookieService } from './_service/cookie.service';
-import { IndexedDBManager } from './_service/storage.service';
-
-export class Auth {
- private static _instance: Auth;
- private _apiService: ApiService;
- private _cookieService: CookieService;
-
- private EXPIRATION_DAYS = 7;
-
- private constructor() {
- this._apiService = new ApiService();
- this._cookieService = new CookieService();
- }
-
- public static getInstance(): Auth {
- if (!Auth._instance) {
- Auth._instance = new Auth();
- }
- return Auth._instance;
- }
-
- public async login(user: user, stayLoggedIn: boolean): Promise {
- return this._apiService
- .logInUser(user)
- .then(response => {
- if (response.status != 'ok') {
- console.log('Something went Wrong');
- }
-
- this._cookieService.new(
- 'token',
- response.token,
- stayLoggedIn ? this.EXPIRATION_DAYS : 0
- );
- this._cookieService.new(
- 'hash',
- response.hash,
- stayLoggedIn ? this.EXPIRATION_DAYS : 0
- );
- return response;
- })
- .catch(err => {
- throw err;
- });
- }
-
- public logout() {
- this._cookieService.delete('token');
- this._cookieService.delete('hash');
- IndexedDBManager.getInstance().then(db => {
- db.deleteAllMessages();
- db.deleteAllUnsentMessages();
- });
- window.navigateTo('/login');
- }
-
- public register(user: user, stayLoggedIn: boolean): Promise {
- return this._apiService
- .registerUser(user)
- .then(response => {
- if (response.status != 'ok') {
- console.log('Something went Wrong');
- }
-
- this._cookieService.new(
- 'token',
- response.token,
- stayLoggedIn ? this.EXPIRATION_DAYS : 0
- );
- this._cookieService.new(
- 'hash',
- response.hash,
- stayLoggedIn ? this.EXPIRATION_DAYS : 0
- );
- return response;
- })
- .catch(err => {
- throw err;
- });
- }
- public deregister() {
- this._apiService
- .deregisterUser(this._cookieService.get('token'))
- .then(response => {
- if (response.status != 'ok') {
- console.log('Something went Wrong');
- }
- this.logout();
- })
- .catch(err => {
- throw err;
- });
- }
-
- public getActiveUser(): Object | null {
- if (this._cookieService.get('token') && this._cookieService.get('hash')) {
- return {
- token: this._cookieService.get('token'),
- hash: this._cookieService.get('hash')
- };
- }
- return null;
- }
-}
diff --git a/src/ts/auth/auth.ts b/src/ts/auth/auth.ts
new file mode 100644
index 0000000..faaa433
--- /dev/null
+++ b/src/ts/auth/auth.ts
@@ -0,0 +1,112 @@
+import { LoginUser, RegisterUser, User } from '../_interface';
+import UserCookie from '../_interface/user/userCookie.interface';
+import { CookieService } from '../_service/cookie/cookie';
+import * as API from '../_service/http';
+import { CONFIG } from '../common';
+
+export class Auth {
+ private static instance: Auth;
+ private cookie: CookieService;
+
+ private constructor() {
+ this.cookie = CookieService.getInstance();
+
+ console.log('[Auth] - instance created');
+ }
+
+ public static getInstance(): Auth {
+ if (!Auth.instance) {
+ Auth.instance = new Auth();
+ }
+
+ return this.instance;
+ }
+
+ public async login(user: LoginUser, stayLoggedIn: boolean): Promise {
+ const response: User | null = await API.login(user);
+
+ if (response === null) {
+ console.log('[Auth]:login - Login failed');
+ return false;
+ }
+
+ const userCookie = {
+ token: response.token,
+ hash: response.hash,
+ userid: user.userid
+ };
+
+ if (stayLoggedIn) {
+ this.cookie.create(
+ CONFIG.COOKIE_TITLE,
+ JSON.stringify(userCookie),
+ CONFIG.COOKIE_EXPIRATION_DAYS
+ );
+ } else {
+ this.cookie.create(CONFIG.COOKIE_TITLE, JSON.stringify(userCookie));
+ }
+
+ console.log('[Auth]:login - Login successful');
+ return true;
+ }
+
+ public async logout() {
+ const response = await API.logout();
+
+ if (response === null || response.status !== 'ok') {
+ console.log('[Auth]:logout - Logout failed');
+ }
+
+ this.cookie.delete(CONFIG.COOKIE_TITLE);
+ console.log('[Auth]:logout - Logout successful');
+ }
+
+ public async register(newUser: RegisterUser): Promise {
+ const user = await API.register(newUser);
+
+ if (user === null) {
+ alert('Register failed');
+ console.log('[Auth]:register - Register failed');
+ return false;
+ }
+
+ const userCookie = {
+ token: user.token,
+ hash: user.hash,
+ userid: newUser.userid
+ };
+
+ this.cookie.create(CONFIG.COOKIE_TITLE, JSON.stringify(userCookie));
+
+ console.log('[Auth]:register - Register successful');
+ return true;
+ }
+
+ public async deregister(password: string): Promise {
+ if (!password) {
+ return false;
+ }
+
+ const cookie = this.cookie.get(CONFIG.COOKIE_TITLE);
+ const token: string = JSON.parse(cookie ? cookie : '').token;
+ const userid: string = JSON.parse(cookie ? cookie : '').userid;
+
+ const response = await API.deregister({ userid, password, token });
+
+ if (response === null || response.status !== 'ok') {
+ alert('Deregister failed');
+ console.log('[Auth]:deregister - Deregister failed');
+ return false;
+ }
+
+ this.cookie.delete(CONFIG.COOKIE_TITLE);
+ console.log('[Auth]:deregister - Deregister successful');
+ return true;
+ }
+
+ public getActiveUser(): UserCookie | null {
+ const userCookie = this.cookie.get(CONFIG.COOKIE_TITLE);
+
+ return userCookie !== null ? JSON.parse(userCookie) : null;
+ }
+}
diff --git a/src/ts/chat.ts b/src/ts/chat.ts
deleted file mode 100644
index 54d9346..0000000
--- a/src/ts/chat.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import { message } from './_interface/message.interface';
-import { ApiService } from './_service/api.service';
-import { IndexedDBManager } from './_service/storage.service';
-import { Auth } from './auth';
-
-export class Chat {
- private _apiService: ApiService;
- private _auth: Auth;
- private pathMessageHtml: string = '/components/message.html';
- private messageHtml: HTMLElement;
-
- constructor() {
- this._apiService = new ApiService();
- this._auth = Auth.getInstance();
- this.getMessageHtml().then(string => {
- const html: Document = new DOMParser().parseFromString(
- string,
- 'text/html'
- );
-
- this.messageHtml = html.body.firstElementChild as HTMLElement;
- });
- }
-
- private async getMessageHtml(): Promise {
- try {
- const response = await fetch(this.pathMessageHtml);
- const html = await response.text();
- return html;
- } catch (error) {
- console.error(error);
- return '';
- }
- }
-
- public init() {
- navigator.serviceWorker.addEventListener('message', e => {
- if (e.data.type == 'fetch-messages') {
- this.fetchMessages();
- }
- });
-
- this.fetchMessages();
- document.getElementById('chat__input').addEventListener('keydown', e => {
- if (e.key == 'Enter' && !e.shiftKey) {
- e.preventDefault();
- this.sendMessage(e.target as HTMLInputElement);
- }
- });
- document.getElementById('chat__input').addEventListener('submit', e => {
- e.preventDefault();
- this.sendMessage(
- (e.target as HTMLFormElement).querySelector('.chat__input__text')
- );
- });
- document.getElementById('header__back').addEventListener('click', () => {
- this._auth.logout();
- });
-
- document.getElementById('deregister').addEventListener('click', e => {
- if (confirm('Are you sure you want to deregister?')) {
- this._auth.deregister();
- }
- });
- document.getElementById('fontsize').addEventListener('input', e => {
- const value = (e.target as HTMLInputElement).value;
- document.body.style.fontSize = `${(+value / 100) * 12}pt`;
- });
- }
-
- private sendMessage(input: HTMLInputElement) {
- const text = input.value.trim();
- input.value = '';
-
- if (text === '') return;
- if (this._auth.getActiveUser() == null) return;
-
- this._apiService
- .sendMessage(this._auth.getActiveUser()['token'], text)
- .then(() => this.fetchMessages())
- .catch(e => {
- console.error("Couldn't send message", e);
-
- IndexedDBManager.getInstance().then(db => {
- db.addUnsentMessage({
- token: this._auth.getActiveUser()['token'],
- text: text
- }).then(() => {
- navigator.serviceWorker.ready.then(registration => {
- registration.sync.register('post-new-messages');
- console.log('sync registered');
- });
- });
- });
- });
- }
-
- public fetchMessages() {
- if (this._auth.getActiveUser() == null) return;
-
- this._apiService
- .fetchMessages(this._auth.getActiveUser()['token'])
- .then(e => {
- this.storeMessages(e.messages);
- this.addMessageNodes(e.messages);
- })
- .catch(err => console.error('Could not fetch messages', err));
- }
-
- private storeMessages(messages: Array) {
- IndexedDBManager.getInstance().then(db => {
- for (let msg of messages) {
- db.addMessage(msg).catch(() => {});
- }
- });
- }
-
- private compareDates(date: string) {
- let dateArr: Array = date.split('_');
- dateArr[1] = dateArr[1].replaceAll('-', ':');
- date = dateArr.join('T') + 'Z';
-
- const messageTimeStamp: Date = new Date(date);
-
- const now: Date = new Date();
- if (now.getTime() - messageTimeStamp.getTime() < 24 * 60 * 60 * 1000) {
- return messageTimeStamp.toLocaleTimeString([], {
- hour: '2-digit',
- minute: '2-digit'
- });
- } else {
- return messageTimeStamp.toLocaleDateString([], {
- month: '2-digit',
- year: '2-digit',
- weekday: 'short'
- });
- }
- }
-
- private addMessageNodes(messages: Array) {
- const chatWindow: HTMLElement = document.getElementById('chat__window');
- const lastMessageId: number = parseInt(
- (chatWindow.lastElementChild as HTMLElement)?.dataset.id || '0'
- );
- const hashOfUser: string = this._auth.getActiveUser()['hash'];
-
- if (chatWindow == null) return;
-
- for (let message of messages) {
- if (message.id <= lastMessageId) continue;
-
- let html: HTMLElement = this.messageHtml.cloneNode(true) as HTMLElement;
-
- html.dataset.id = message.id.toString();
- html.dataset.chatId = message.id.toString();
- html.dataset.userHash = message.userhash;
- html.querySelector('.message__title__name').innerHTML =
- message.usernickname;
- const time = new Date(message.time);
- html.querySelector('.message__title__time').innerHTML = this.compareDates(
- message.time
- );
- html.querySelector('.message__content').innerHTML = message.text;
-
- if (message.userhash === hashOfUser) html.classList.add('myself');
-
- chatWindow.appendChild(html);
-
- chatWindow.scrollTo(0, chatWindow.scrollHeight);
- }
- }
-}
diff --git a/src/ts/common.ts b/src/ts/common.ts
new file mode 100644
index 0000000..f8f930b
--- /dev/null
+++ b/src/ts/common.ts
@@ -0,0 +1,54 @@
+import { Route } from './_interface';
+
+export const CONFIG = {
+ COOKIE_EXPIRATION_DAYS: 30,
+ COOKIE_TITLE: 'user',
+ BASE_URL_API: 'https://www2.hs-esslingen.de/~melcher/map/chat/api/',
+ HELP_URL: 'https://github.com/FreakeyPlays/pwa-chat#readme'
+};
+
+export const ROUTES: { [key: string]: Route } = {
+ '404': {
+ id: -1,
+ path: '/pages/404.html',
+ title: '404',
+ loginRequired: false
+ },
+ '/': {
+ id: 0,
+ path: '/pages/chat.html',
+ title: 'Chat',
+ loginRequired: true
+ },
+ '/login': {
+ id: 1,
+ path: '/pages/login.html',
+ title: 'Login',
+ loginRequired: false
+ }
+};
+
+export const LOGIN_CRITERIA = {
+ userid: [
+ {
+ validate(data: string): boolean {
+ return data.length > 0;
+ },
+ errorMessage: "User ID can't be empty"
+ },
+ {
+ validate(data: string): boolean {
+ return data.match(/^[a-zA-Z]{6}[0-9]{2}$/) !== null;
+ },
+ errorMessage: 'User ID must be HSE Username'
+ }
+ ],
+ password: [
+ {
+ validate(data: string): boolean {
+ return data.length > 0;
+ },
+ errorMessage: "Password can't be empty"
+ }
+ ]
+};
diff --git a/src/ts/logger.ts b/src/ts/logger.ts
new file mode 100644
index 0000000..38176d5
--- /dev/null
+++ b/src/ts/logger.ts
@@ -0,0 +1,29 @@
+export class Logger {
+ private static instance: Logger;
+ private oldConsoleLog: any = null;
+
+ private constructor() {
+ console.log('[Logger] - instance created');
+ }
+
+ static getInstance() {
+ if (!Logger.instance) {
+ Logger.instance = new Logger();
+ }
+ return this.instance;
+ }
+
+ public enableLogging() {
+ if (this.oldConsoleLog === null) {
+ return;
+ }
+
+ window['console']['log'] = this.oldConsoleLog;
+ }
+
+ public disableLogging() {
+ this.oldConsoleLog = console.log;
+
+ window['console']['log'] = function () {};
+ }
+}
diff --git a/src/ts/messenger/features/camera.ts b/src/ts/messenger/features/camera.ts
new file mode 100644
index 0000000..377fbeb
--- /dev/null
+++ b/src/ts/messenger/features/camera.ts
@@ -0,0 +1,72 @@
+export class Camera {
+ private static instance: Camera;
+
+ private previewElement!: HTMLVideoElement;
+ private stream!: MediaStream;
+
+ private pictureWidth = 640;
+ private pictureHeight = 480;
+
+ private constructor() {
+ console.log('[Camera] - instance created');
+ }
+
+ public static getInstance(): Camera {
+ if (!Camera.instance) {
+ Camera.instance = new Camera();
+ }
+
+ return this.instance;
+ }
+
+ public init(preview: HTMLVideoElement): void {
+ this.previewElement = preview;
+ }
+
+ public start(): void {
+ navigator.mediaDevices
+ .getUserMedia({
+ video: {
+ width: this.pictureWidth,
+ height: this.pictureHeight,
+ facingMode: 'user'
+ },
+ audio: false
+ })
+ .then(stream => {
+ this.stream = stream;
+ this.previewElement.srcObject = stream;
+
+ console.log('[Camera] - camera started');
+ })
+ .catch(err => {
+ console.log('[Camera] - a error occurred\n' + err);
+ });
+ }
+
+ public stop(): void {
+ if (!this.stream) return;
+
+ this.previewElement.pause();
+ this.stream.getTracks().forEach(track => {
+ track.stop();
+ });
+
+ console.log('[Camera] - camera stopped');
+ }
+
+ public takePicture(): string | null {
+ if (!this.stream) return null;
+
+ const canvas = document.createElement('canvas');
+ canvas.width = this.pictureWidth;
+ canvas.height = this.pictureHeight;
+
+ let context = canvas.getContext('2d') as CanvasRenderingContext2D;
+ context.drawImage(this.previewElement, 0, 0, canvas.width, canvas.height);
+
+ const picture = canvas.toDataURL('image/png');
+
+ return picture;
+ }
+}
diff --git a/src/ts/messenger/features/speechToText.ts b/src/ts/messenger/features/speechToText.ts
new file mode 100644
index 0000000..ead5832
--- /dev/null
+++ b/src/ts/messenger/features/speechToText.ts
@@ -0,0 +1,65 @@
+export class SpeechToText {
+ private static instance: SpeechToText;
+ private speechRecognition: any;
+
+ private outputElement!: HTMLTextAreaElement;
+ private currentlyRecording = false;
+
+ private constructor() {
+ console.log('[SpeechToText] - instance created');
+ }
+
+ public static getInstance(): SpeechToText {
+ if (!SpeechToText.instance) {
+ SpeechToText.instance = new SpeechToText();
+ }
+
+ return this.instance;
+ }
+
+ public init(outputElement: HTMLTextAreaElement): void {
+ this.speechRecognition = new window.webkitSpeechRecognition();
+
+ this.speechRecognition.lang = 'de-DE'; //navigator.language || 'en-US';
+ this.speechRecognition.interimResults = true;
+ this.speechRecognition.continuous = true;
+
+ this.outputElement = outputElement;
+
+ this.speechRecognition.addEventListener('result', (e: any) => {
+ this.resultHandler(e);
+ });
+ this.speechRecognition.addEventListener('end', () => {
+ this.endHandler();
+ });
+
+ console.log('[SpeechToText] - initialized SpeechToText');
+ }
+
+ private resultHandler(e: any): void {
+ const transcript = Array.from(e.results)
+ .map((result: any) => result[0])
+ .map((result: any) => result.transcript)
+ .join('');
+
+ this.outputElement.innerText = transcript;
+ console.log('[SpeechToText] - result handled');
+ }
+
+ private endHandler(): void {
+ console.log('[SpeechToText] - speech recognition ended');
+ }
+
+ public start(): void {
+ if (this.currentlyRecording) return;
+
+ this.currentlyRecording = true;
+ this.speechRecognition.start();
+ console.log('[SpeechToText] - speech recognition started');
+ }
+
+ public stop(): void {
+ this.speechRecognition.stop();
+ console.log('[SpeechToText] - speech recognition stopped');
+ }
+}
diff --git a/src/ts/messenger/messenger.ts b/src/ts/messenger/messenger.ts
new file mode 100644
index 0000000..94b794f
--- /dev/null
+++ b/src/ts/messenger/messenger.ts
@@ -0,0 +1,195 @@
+import { Message, SendMessage } from '../_interface';
+import { CreateMessage } from '../_interface/message/createMessage.interface';
+import * as API from '../_service/http';
+import { IndexedDBManager } from '../_service/idb/idb';
+import { CONFIG } from '../common';
+import { MessageConstructor } from './nodes/messageConstructor';
+import { MessengerSettings } from './settings/settings';
+
+export class Messenger {
+ private static instance: Messenger;
+ private settings: MessengerSettings;
+ private messageConstructor: MessageConstructor;
+ private idb: IndexedDBManager;
+
+ private chatWindow!: HTMLElement;
+ private lastMessageId: number;
+ private isOnline: boolean;
+
+ private constructor() {
+ this.messageConstructor = MessageConstructor.getInstance();
+ this.settings = MessengerSettings.getInstance();
+ this.idb = IndexedDBManager.getInstance();
+ this.lastMessageId = 0;
+ this.isOnline = navigator.onLine;
+
+ console.log('[Messenger] - instance created');
+ }
+
+ public static getInstance(): Messenger {
+ if (!Messenger.instance) {
+ Messenger.instance = new Messenger();
+ }
+ return Messenger.instance;
+ }
+
+ public init(): void {
+ this.settings.init();
+ this.chatWindow = document.getElementById('chat__window') as HTMLElement;
+ this.fetchMessages();
+
+ window.addEventListener('online', () => {
+ console.log('[Messenger] - online event fired');
+ this.isOnline = true;
+ this.syncUnsentMessages();
+ this.fetchMessages();
+ });
+
+ window.addEventListener('offline', () => {
+ console.log('[Messenger] - offline event fired');
+ this.isOnline = false;
+ });
+
+ this.setupHelpButton();
+ console.log('[Messenger] - initialized');
+ }
+
+ public async saveUnsentMessage(message: SendMessage): Promise {
+ const success = await this.idb.unsentMessages().add(message);
+
+ if (success) {
+ console.log('[Messenger]:saveUnsentMessage - Message saved');
+ } else {
+ console.log('[Messenger]:saveUnsentMessage - Something went Wrong');
+ }
+ }
+
+ public async syncUnsentMessages(): Promise {
+ const unsentMessages = await this.idb.unsentMessages().getAll();
+
+ if (unsentMessages.length === 0) {
+ return;
+ }
+
+ for (const unsentMessage of unsentMessages) {
+ const success = await this.sendMessage(unsentMessage);
+
+ if (success) {
+ await this.idb.unsentMessages().delete(unsentMessage.id);
+ }
+ }
+
+ console.log('[Messenger]:syncUnsentMessages - Messages synced');
+ }
+
+ public async sendMessage(message: SendMessage): Promise {
+ if (!this.isOnline) {
+ await this.saveUnsentMessage(message);
+ return true;
+ }
+
+ const response = await API.sendMessage(message);
+
+ if (response === null) {
+ console.log('[Messenger]:sendMessage - Something went Wrong');
+ return false;
+ }
+
+ return response ? true : false;
+ }
+
+ private async loadLocalMessages(): Promise {
+ const createMessages: CreateMessage[] = await this.idb.messages().getAll();
+ for (let createMessage of createMessages) {
+ const message: Message = {
+ id: createMessage.id,
+ chatid: createMessage.chatid,
+ userhash: createMessage.userhash,
+ usernickname: createMessage.usernickname,
+ time: createMessage.time
+ };
+
+ if (createMessage.text) {
+ message.text = createMessage.text;
+ }
+
+ if (createMessage.photo) {
+ message.photoid = URL.createObjectURL(createMessage.photo);
+ }
+
+ const html = this.messageConstructor.createNode(message);
+ this.chatWindow.innerHTML = html + this.chatWindow.innerHTML;
+
+ this.lastMessageId = message.id || 0;
+ }
+ }
+
+ private async saveMessageLocal(message: Message): Promise {
+ const createMessage: CreateMessage = {
+ id: message.id,
+ chatid: message.chatid,
+ userhash: message.userhash,
+ usernickname: message.usernickname,
+ time: message.time
+ };
+
+ if (message.text) {
+ createMessage.text = message.text;
+ }
+
+ if (message.photoid) {
+ const blob = await fetch(message.photoid).then(r => r.blob());
+ createMessage.photo = blob;
+ }
+
+ this.idb.messages().add(createMessage);
+ }
+
+ public async fetchMessages(): Promise {
+ await this.loadLocalMessages();
+
+ const messages = await API.fetchMessages(this.lastMessageId);
+
+ if (messages === null) {
+ console.log('[Messenger]:fetchMessages - Something went Wrong');
+ return;
+ }
+
+ for (const message of messages) {
+ if (message.photoid) {
+ message.photoid = await this.fetchPhoto(message.photoid);
+ }
+
+ const html = this.messageConstructor.createNode(message);
+ this.chatWindow.innerHTML = html + this.chatWindow.innerHTML;
+
+ this.saveMessageLocal(message);
+ }
+
+ if (messages?.length === 0) {
+ return;
+ }
+
+ this.lastMessageId = messages[messages.length - 1].id || 0;
+ this.fetchMessages();
+ }
+
+ public async fetchPhoto(photoid: string): Promise {
+ const response = await API.fetchPhoto(photoid);
+
+ if (response === null) {
+ console.log('[Messenger]:fetchPhoto - Something went Wrong');
+ return '';
+ }
+
+ return URL.createObjectURL(response);
+ }
+
+ private setupHelpButton(): void {
+ const helpButton = document.getElementById('help') as HTMLElement;
+
+ helpButton.addEventListener('click', () => {
+ window.open(CONFIG.HELP_URL, '_blank')?.focus();
+ });
+ }
+}
diff --git a/src/ts/messenger/nodes/messageConstructor.ts b/src/ts/messenger/nodes/messageConstructor.ts
new file mode 100644
index 0000000..ab8a88a
--- /dev/null
+++ b/src/ts/messenger/nodes/messageConstructor.ts
@@ -0,0 +1,142 @@
+import { Message } from '../../_interface';
+import { Auth } from '../../auth/auth';
+
+export class MessageConstructor {
+ private static instance: MessageConstructor;
+ private auth: Auth;
+
+ private latestDayTag: Date;
+ private currentSender: string;
+ private currentUser!: string;
+
+ constructor() {
+ this.auth = Auth.getInstance();
+ this.currentSender = '';
+ this.latestDayTag = new Date(1970, 1, 1);
+
+ console.log('[messageNode] - instance created');
+ }
+
+ public static getInstance(): MessageConstructor {
+ if (!MessageConstructor.instance) {
+ MessageConstructor.instance = new MessageConstructor();
+ }
+ return MessageConstructor.instance;
+ }
+
+ public createNode(message: Message): string {
+ this.currentUser = this.auth.getActiveUser()?.hash || '';
+ let dateChanged = false;
+ let myself = false;
+ let sameSender = true;
+ let emojiOnly = false;
+ const { dateObj, date, time } = this.parseDate(message.time);
+
+ if (message.userhash === this.currentUser) {
+ myself = true;
+ }
+
+ if (message.userhash !== this.currentSender) {
+ this.currentSender = message.userhash;
+ sameSender = false;
+ }
+
+ if (this.compareDates(dateObj, this.latestDayTag)) {
+ this.latestDayTag = dateObj;
+ dateChanged = true;
+ sameSender = false;
+ }
+
+ if (
+ !message.photoid &&
+ message.text &&
+ this.checkIfEmojiOnly(message.text)
+ ) {
+ emojiOnly = true;
+ }
+
+ return dateChanged
+ ? this.getMessageNode(myself, sameSender, emojiOnly, message, time) +
+ this.getDividerNode(date)
+ : this.getMessageNode(myself, sameSender, emojiOnly, message, time);
+ }
+
+ private checkIfEmojiOnly(text: string): boolean {
+ const emojiRegex =
+ /^(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c[\ude32-\ude3a]|[\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])+$/;
+
+ return text.match(emojiRegex) ? true : false;
+ }
+
+ private compareDates(date1: Date, date2: Date): boolean {
+ date1.setHours(0, 0, 0, 0);
+ date2.setHours(0, 0, 0, 0);
+
+ return date1 > date2;
+ }
+
+ private parseDate(dateString: string) {
+ const [datePart, timePart] = dateString.split('_');
+ const [year, month, day] = datePart.split('-');
+ const [hour, minute, second] = timePart.split('-');
+
+ const dateObj = new Date(
+ Number(year),
+ Number(month) - 1,
+ Number(day),
+ Number(hour),
+ Number(minute),
+ Number(second)
+ );
+ const time = dateObj.toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ const date = dateObj.toLocaleDateString([], {
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric'
+ });
+
+ return { dateObj, date, time };
+ }
+
+ private randomColorBasedOnHash(hash: string): string {
+ let random = 0;
+ for (let i = 0; i < hash.length; i++) {
+ random = hash.charCodeAt(i) + ((random << 5) - random);
+ }
+
+ return `hsl(${random % 360}, 85%, 50%)`;
+ }
+
+ private getDividerNode(date: string): string {
+ return `${date}
`;
+ }
+
+ public getMessageNode(
+ myself: boolean,
+ sameSender: boolean,
+ emojiOnly: boolean,
+ message: Message,
+ time: string
+ ): string {
+ return `${
+ message.text ? message.text : ''
+ }${time}
`;
+ }
+}
diff --git a/src/ts/messenger/settings/functions/deregister.ts b/src/ts/messenger/settings/functions/deregister.ts
new file mode 100644
index 0000000..dac4ba7
--- /dev/null
+++ b/src/ts/messenger/settings/functions/deregister.ts
@@ -0,0 +1,58 @@
+import { IndexedDBManager } from '../../../_service/idb/idb';
+import { Auth } from '../../../auth/auth';
+
+export class DeregisterFunction {
+ private static instance: DeregisterFunction;
+ private auth: Auth;
+ private idb: IndexedDBManager;
+
+ private constructor() {
+ this.auth = Auth.getInstance();
+ this.idb = IndexedDBManager.getInstance();
+
+ console.log('[DeregisterFunction] - instance created');
+ }
+
+ public static getInstance(): DeregisterFunction {
+ if (!DeregisterFunction.instance) {
+ DeregisterFunction.instance = new DeregisterFunction();
+ }
+ return DeregisterFunction.instance;
+ }
+
+ public init(): void {
+ this.setup();
+
+ console.log('[DeregisterFunction] - initialized');
+ }
+
+ private setup() {
+ const deregisterButton = document.getElementById(
+ 'deregister'
+ ) as HTMLButtonElement;
+ deregisterButton.addEventListener('click', () => {
+ this.deregister();
+ });
+ }
+
+ private async deregister() {
+ const password = prompt(
+ 'Please provide your Password to Delete your Account.'
+ );
+
+ if (!password) return;
+
+ const success = await this.auth.deregister(password);
+
+ if (!success) {
+ return;
+ }
+
+ this.idb.settings().clear();
+ this.idb.messages().clear();
+ this.idb.unsentMessages().clear();
+ this.idb.wallpaper().clear();
+
+ window.location.reload();
+ }
+}
diff --git a/src/ts/messenger/settings/functions/logout.ts b/src/ts/messenger/settings/functions/logout.ts
new file mode 100644
index 0000000..ceae9d6
--- /dev/null
+++ b/src/ts/messenger/settings/functions/logout.ts
@@ -0,0 +1,46 @@
+import { IndexedDBManager } from '../../../_service/idb/idb';
+import { Auth } from '../../../auth/auth';
+
+export class LogoutFunction {
+ private static instance: LogoutFunction;
+ private auth: Auth;
+ private idb: IndexedDBManager;
+
+ private constructor() {
+ this.auth = Auth.getInstance();
+ this.idb = IndexedDBManager.getInstance();
+
+ console.log('[LogoutFunction] - instance created');
+ }
+
+ public static getInstance(): LogoutFunction {
+ if (!LogoutFunction.instance) {
+ LogoutFunction.instance = new LogoutFunction();
+ }
+ return LogoutFunction.instance;
+ }
+
+ public init(): void {
+ this.setup();
+
+ console.log('[LogoutFunction] - initialized');
+ }
+
+ private setup() {
+ const logoutButton = document.getElementById('logout') as HTMLElement;
+ logoutButton.addEventListener('click', () => {
+ this.logout();
+ });
+ }
+
+ private logout() {
+ this.idb.settings().clear();
+ this.idb.messages().clear();
+ this.idb.unsentMessages().clear();
+ this.idb.wallpaper().clear();
+
+ this.auth.logout();
+
+ window.location.reload();
+ }
+}
diff --git a/src/ts/messenger/settings/setting/font.ts b/src/ts/messenger/settings/setting/font.ts
new file mode 100644
index 0000000..8378b1e
--- /dev/null
+++ b/src/ts/messenger/settings/setting/font.ts
@@ -0,0 +1,89 @@
+import { IndexedDBManager } from '../../../_service/idb/idb';
+
+export class FontSetting {
+ private static instance: FontSetting;
+ private idb: IndexedDBManager;
+
+ private currentSize: 's' | 'm' | 'l' = 'm';
+ private rootElement!: HTMLElement;
+ private buttons: HTMLButtonElement[] = new Array();
+
+ private constructor() {
+ this.idb = IndexedDBManager.getInstance();
+
+ console.log('[FontSetting] - instance created');
+ }
+
+ public static getInstance(): FontSetting {
+ if (!FontSetting.instance) {
+ FontSetting.instance = new FontSetting();
+ }
+
+ return FontSetting.instance;
+ }
+
+ public init(currentSetting: { current: 's' | 'm' | 'l' }): void {
+ this.setup();
+
+ if (currentSetting.current !== 'm') {
+ this.toggleFont(currentSetting.current);
+ }
+
+ console.log('[FontSetting] - initialized FontSetting');
+ }
+
+ public setup(): void {
+ const container = document.getElementById(
+ 'setting-font-container'
+ ) as HTMLElement;
+ this.rootElement = document.documentElement as HTMLElement;
+ const buttons = container.children;
+
+ for (let i = 0; i < buttons.length; i++) {
+ const button = buttons[i] as HTMLButtonElement;
+ this.buttons.push(button);
+ button.addEventListener('click', () => {
+ this.toggleFont(button.dataset.fontSize as 's' | 'm' | 'l');
+ });
+ }
+ }
+
+ private toggleFont(size: 's' | 'm' | 'l'): void {
+ if (this.currentSize === size) return;
+
+ this.currentSize = size;
+
+ this.buttons.forEach(button => {
+ if (button.dataset.fontSize === size) {
+ button.classList.add('active');
+ } else {
+ button.classList.remove('active');
+ }
+ });
+
+ this.rootElement.classList.remove('text-size-s', 'text-size-l');
+ if (size !== 'm') {
+ this.rootElement.classList.add(`text-size-${size}`);
+ }
+
+ this.updateSetting();
+
+ console.log('[FontSetting] - toggled font size');
+ }
+
+ private async updateSetting(): Promise {
+ const stored = await this.idb.settings().get(1);
+
+ if (!stored) return;
+
+ stored.fontSize.current = this.currentSize;
+
+ const response = await this.idb.settings().update(stored);
+
+ if (response) {
+ console.log('[FontSetting] - updated setting');
+ } else {
+ console.log('[FontSetting] - failed to update setting');
+ }
+ }
+}
diff --git a/src/ts/messenger/settings/setting/theme.ts b/src/ts/messenger/settings/setting/theme.ts
new file mode 100644
index 0000000..6be6a3d
--- /dev/null
+++ b/src/ts/messenger/settings/setting/theme.ts
@@ -0,0 +1,152 @@
+import { ThemeOption } from '../../../_interface/settings/settings';
+import { IndexedDBManager } from '../../../_service/idb/idb';
+
+export class ThemeSetting {
+ private static instance: ThemeSetting;
+ private idb: IndexedDBManager;
+
+ private rootElement!: HTMLElement;
+ private currentTheme: 'light' | 'dark' | null = 'light';
+ private currentAccentColor: 'green' | 'purple' = 'green';
+
+ private themeToggle!: HTMLElement;
+ private accentColorButtons: HTMLElement[] = new Array();
+
+ private constructor() {
+ this.idb = IndexedDBManager.getInstance();
+
+ console.log('[ThemeSetting] - instance created');
+ }
+
+ public static getInstance(): ThemeSetting {
+ if (!ThemeSetting.instance) {
+ ThemeSetting.instance = new ThemeSetting();
+ }
+
+ return ThemeSetting.instance;
+ }
+
+ public init(currentSetting: ThemeOption): void {
+ this.setup();
+
+ if (currentSetting.design === null) {
+ window
+ .matchMedia('(prefers-color-scheme: dark)')
+ .addEventListener('change', event => {
+ this.handleSystemPreference(event.matches ? 'dark' : 'light');
+ });
+
+ const preference = window.matchMedia('(prefers-color-scheme: dark)')
+ .matches
+ ? 'dark'
+ : 'light';
+
+ this.handleSystemPreference(preference);
+ }
+
+ if (currentSetting.design === 'dark') {
+ this.toggleTheme();
+ }
+
+ if (currentSetting.accent === 'purple') {
+ this.toggleAccentColor('purple');
+ }
+
+ console.log('[ThemeSetting] - initialized ThemeSetting');
+ }
+
+ public setup(): void {
+ this.rootElement = document.body as HTMLElement;
+
+ this.themeToggle = document.getElementById(
+ 'setting-theme-toggle'
+ ) as HTMLElement;
+ const accentContainer = document.getElementById(
+ 'setting-accent-container'
+ ) as HTMLElement;
+
+ this.themeToggle.addEventListener('input', () => {
+ this.toggleTheme();
+ });
+
+ const accentButtons = accentContainer.children;
+
+ for (let i = 0; i < accentButtons.length; i++) {
+ const element = accentButtons[i] as HTMLButtonElement;
+ this.accentColorButtons.push(element);
+
+ element.addEventListener('click', () => {
+ this.toggleAccentColor(element.dataset.color as 'green' | 'purple');
+ });
+ }
+ }
+
+ public handleSystemPreference(theme: 'light' | 'dark'): void {
+ this.currentTheme = theme;
+
+ if (theme === 'dark') {
+ this.setToggleToState(true);
+ this.rootElement.classList.add('theme--dark');
+ } else {
+ this.setToggleToState(false);
+ this.rootElement.classList.remove('theme--dark');
+ }
+
+ console.log('[ThemeSetting] - toggled theme via system preference');
+ }
+
+ public setToggleToState(state: boolean): void {
+ (
+ this.themeToggle.querySelector(
+ "input[type='checkbox']"
+ ) as HTMLInputElement
+ ).checked = state;
+ }
+
+ public async toggleTheme(): Promise {
+ if (this.currentTheme === 'dark') {
+ this.rootElement.classList.remove('theme--dark');
+ this.currentTheme = 'light';
+ this.setToggleToState(false);
+ } else {
+ this.rootElement.classList.add('theme--dark');
+ this.currentTheme = 'dark';
+ this.setToggleToState(true);
+ }
+
+ await this.updateSettings();
+
+ console.log('[ThemeSetting] - toggled theme');
+ }
+
+ public async toggleAccentColor(color: 'green' | 'purple'): Promise {
+ if (this.currentAccentColor === color) return;
+
+ this.accentColorButtons.forEach(element => {
+ element.classList.toggle('active');
+ });
+ this.rootElement.classList.toggle('theme--accent--purple');
+ this.currentAccentColor = color;
+
+ await this.updateSettings();
+
+ console.log('[ThemeSetting] - toggled accent color');
+ }
+
+ public async updateSettings() {
+ const stored = await this.idb.settings().get(1);
+
+ stored.theme = {
+ design: this.currentTheme,
+ accent: this.currentAccentColor
+ };
+
+ const response = await this.idb.settings().update(stored);
+
+ if (response) {
+ console.log('[ThemeSetting] - updated settings');
+ } else {
+ console.log('[ThemeSetting] - failed to update settings');
+ }
+ }
+}
diff --git a/src/ts/messenger/settings/setting/wallpaper.ts b/src/ts/messenger/settings/setting/wallpaper.ts
new file mode 100644
index 0000000..2838ebf
--- /dev/null
+++ b/src/ts/messenger/settings/setting/wallpaper.ts
@@ -0,0 +1,150 @@
+import { WallpaperOption } from '../../../_interface/settings/settings';
+import { IndexedDBManager } from '../../../_service/idb/idb';
+
+export class WallpaperSetting {
+ private static instance: WallpaperSetting;
+ private idb: IndexedDBManager;
+
+ private background!: HTMLElement;
+ private currentWallpaper: WallpaperOption = { current: null };
+ private wallpaperContainer!: HTMLElement;
+
+ private constructor() {
+ this.idb = IndexedDBManager.getInstance();
+
+ console.log('[WallpaperSetting] - instance created');
+ }
+
+ public static getInstance(): WallpaperSetting {
+ if (!WallpaperSetting.instance) {
+ WallpaperSetting.instance = new WallpaperSetting();
+ }
+
+ return WallpaperSetting.instance;
+ }
+
+ public init(currentSetting: WallpaperOption): void {
+ this.setup();
+
+ if (currentSetting.current) {
+ this.currentWallpaper = currentSetting;
+ this.setWallpaper(currentSetting.current);
+ }
+
+ this.updateWallpaperPreview();
+ console.log('[WallpaperSetting] - initialized WallpaperSetting');
+ }
+
+ public setup(): void {
+ this.background = document.getElementById('chat__window') as HTMLElement;
+ this.wallpaperContainer = document.getElementById(
+ 'wallpaper__container'
+ ) as HTMLElement;
+ const wallpaperReset = document.getElementById(
+ 'wallpaper__reset'
+ ) as HTMLElement;
+
+ for (let i = 0; i < this.wallpaperContainer.children.length; i++) {
+ const wallpaperPreview = this.wallpaperContainer.children[
+ i
+ ] as HTMLElement;
+
+ if (wallpaperPreview.dataset.wallpaperId) {
+ wallpaperPreview.addEventListener('click', e => {
+ this.setWallpaper(
+ Number(wallpaperPreview.dataset.wallpaperId) as 1 | 2 | 3
+ );
+ });
+
+ continue;
+ }
+
+ const fileInput = wallpaperPreview.querySelector(
+ 'input'
+ ) as HTMLInputElement;
+
+ fileInput.addEventListener('change', () => {
+ if (!fileInput.files) return;
+
+ const image: File = fileInput.files[0];
+ this.saveWallpaper(image);
+ });
+ }
+
+ wallpaperReset.addEventListener('click', () => {
+ this.resetWallpaper();
+ });
+ }
+
+ public async saveWallpaper(newImage: File) {
+ const storedWallpaper = await this.idb.wallpaper().getAll();
+ console.log(storedWallpaper);
+
+ if (storedWallpaper[1]) {
+ const wallpaperTwo = storedWallpaper[1];
+ await this.idb.wallpaper().update({ id: 3, image: wallpaperTwo.image });
+ }
+ if (storedWallpaper[0]) {
+ const wallpaperOne = storedWallpaper[0];
+ await this.idb.wallpaper().update({ id: 2, image: wallpaperOne.image });
+ }
+
+ await this.idb.wallpaper().update({ id: 1, image: newImage });
+
+ this.updateWallpaperPreview();
+ this.currentWallpaper = { current: 1 };
+ await this.updateSetting();
+ this.setWallpaper(1);
+ }
+
+ public async updateWallpaperPreview(): Promise {
+ const storedWallpaper = await this.idb.wallpaper().getAll();
+
+ for (let i = 0; i < storedWallpaper.length; i++) {
+ const wallpaperPreview = this.wallpaperContainer.querySelector(
+ `[data-wallpaper-id="${i + 1}"]`
+ ) as HTMLElement;
+
+ if (storedWallpaper[i]) {
+ wallpaperPreview.style.backgroundImage = `url(${URL.createObjectURL(
+ storedWallpaper[i].image
+ )})`;
+
+ if (wallpaperPreview.classList.contains('empty')) {
+ wallpaperPreview.classList.remove('empty');
+ }
+ } else {
+ wallpaperPreview.style.backgroundImage = 'none';
+ }
+ }
+ }
+
+ public async setWallpaper(id: 1 | 2 | 3): Promise {
+ this.currentWallpaper = { current: id };
+ const image = await this.idb.wallpaper().get(id);
+ this.background.style.backgroundImage = `url(${URL.createObjectURL(
+ image.image
+ )})`;
+ this.updateSetting();
+ }
+
+ public resetWallpaper(): void {
+ this.currentWallpaper = { current: null };
+ this.background.style.backgroundImage = 'none';
+ this.updateSetting();
+ }
+
+ public async updateSetting() {
+ const stored = await this.idb.settings().get(1);
+
+ stored.wallpaper = this.currentWallpaper;
+
+ const response = await this.idb.settings().update(stored);
+
+ if (response) {
+ console.log('[WallpaperSetting] - updated wallpaper setting');
+ } else {
+ console.log('[WallpaperSetting] - failed to update wallpaper setting');
+ }
+ }
+}
diff --git a/src/ts/messenger/settings/settings.ts b/src/ts/messenger/settings/settings.ts
new file mode 100644
index 0000000..6e39c7d
--- /dev/null
+++ b/src/ts/messenger/settings/settings.ts
@@ -0,0 +1,82 @@
+import { Setting } from '../../_interface/settings/setting.interface';
+import { IndexedDBManager } from '../../_service/idb/idb';
+import { DeregisterFunction } from './functions/deregister';
+import { LogoutFunction } from './functions/logout';
+import { FontSetting } from './setting/font';
+import { ThemeSetting } from './setting/theme';
+import { WallpaperSetting } from './setting/wallpaper';
+
+export class MessengerSettings {
+ private static instance: MessengerSettings;
+ private idb: IndexedDBManager;
+
+ private wallpaperSetting: WallpaperSetting;
+ private themeSetting: ThemeSetting;
+ private fontSizeSetting: FontSetting;
+
+ private logoutFunction: LogoutFunction;
+ private deregisterFunction: DeregisterFunction;
+
+ private currentSetting!: Setting;
+
+ private constructor() {
+ this.idb = IndexedDBManager.getInstance();
+
+ this.wallpaperSetting = WallpaperSetting.getInstance();
+ this.themeSetting = ThemeSetting.getInstance();
+ this.fontSizeSetting = FontSetting.getInstance();
+
+ this.logoutFunction = LogoutFunction.getInstance();
+ this.deregisterFunction = DeregisterFunction.getInstance();
+
+ console.log('[MessengerSettings] - instance created');
+ }
+
+ public static getInstance(): MessengerSettings {
+ if (!MessengerSettings.instance) {
+ MessengerSettings.instance = new MessengerSettings();
+ }
+
+ return MessengerSettings.instance;
+ }
+
+ public async init(): Promise {
+ await this.setupSettings();
+
+ this.themeSetting.init(this.currentSetting.theme);
+ this.wallpaperSetting.init(this.currentSetting.wallpaper);
+ this.fontSizeSetting.init(this.currentSetting.fontSize);
+
+ this.logoutFunction.init();
+ this.deregisterFunction.init();
+
+ console.log('[MessengerSettings] - initialized MessengerSettings');
+ }
+
+ private async setupSettings(): Promise {
+ this.currentSetting = await this.idb.settings().get(1);
+
+ if (this.currentSetting) return;
+
+ this.currentSetting = {
+ theme: {
+ design: null,
+ accent: 'green'
+ },
+ wallpaper: {
+ current: null
+ },
+ fontSize: {
+ current: 'm'
+ }
+ };
+
+ const response = await this.idb.settings().add(this.currentSetting);
+
+ if (response) {
+ console.log('[MessengerSettings] - added default settings');
+ } else {
+ console.log('[MessengerSettings] - failed to add default settings');
+ }
+ }
+}
diff --git a/src/ts/pages/chat.page.ts b/src/ts/pages/chat.page.ts
new file mode 100644
index 0000000..eccae79
--- /dev/null
+++ b/src/ts/pages/chat.page.ts
@@ -0,0 +1,385 @@
+import { SendMessage } from '../_interface';
+import { ROUTES } from '../common';
+import { Camera } from '../messenger/features/camera';
+import { SpeechToText } from '../messenger/features/speechToText';
+import { Messenger } from '../messenger/messenger';
+
+export class ChatPage {
+ private static instance: ChatPage;
+ private camera: Camera;
+ private speechToText: SpeechToText;
+ private messenger: Messenger;
+
+ private picture: string = '';
+
+ private constructor() {
+ this.camera = Camera.getInstance();
+ this.speechToText = SpeechToText.getInstance();
+ this.messenger = Messenger.getInstance();
+
+ document.addEventListener('spaContentLoaded', e => {
+ if ((e).detail.pageId === ROUTES['/'].id) {
+ this.init();
+ }
+ });
+
+ console.log('[ChatPage] - constructed ChatPage');
+ }
+
+ public static getInstance(): ChatPage {
+ if (!ChatPage.instance) {
+ ChatPage.instance = new ChatPage();
+ }
+
+ return ChatPage.instance;
+ }
+
+ public init(): void {
+ this.messenger.init();
+ ////
+ this.setupComposeMessage();
+ this.setChatEventListener();
+ this.setOptionsToggle();
+ this.setupFilterMessages();
+ ////
+ console.log('[ChatPage] - initialized ChatPage');
+ }
+
+ private setChatEventListener(): void {
+ const chatWindow = document.getElementById('chat__window');
+
+ chatWindow?.addEventListener('click', e => {
+ const target = e.target as HTMLElement;
+
+ if (target.dataset.view === 'fullscreen') {
+ this.openImageInFullSize(target as HTMLImageElement);
+ }
+ });
+
+ console.log('[ChatPage] - activated chat event listener');
+ }
+
+ private setupSpeechToText(): void {
+ const cameraPresent = 'mediaDevices' in navigator;
+
+ if (!cameraPresent) return;
+
+ const output = document.getElementById(
+ 'compose__container__input'
+ ) as HTMLTextAreaElement;
+ const microphoneToggle = document.getElementById('microphone__toggle');
+
+ this.speechToText.init(output);
+
+ microphoneToggle?.addEventListener('click', () => {
+ this.toggleSpeechToText(microphoneToggle);
+ });
+
+ console.log('[ChatPage] - successful setup SpeechToText');
+ }
+
+ private toggleSpeechToText(toggle: HTMLElement): void {
+ if (toggle.classList.contains('recording')) {
+ toggle.classList.remove('recording');
+ this.speechToText.stop();
+ console.log('[ChatPage] - speech recognition stopped');
+ } else {
+ toggle.classList.add('recording');
+ this.speechToText.start();
+ console.log('[ChatPage] - speech recognition started');
+ }
+ }
+
+ private setupCamera(): void {
+ const cameraPresent = 'mediaDevices' in navigator;
+
+ if (!cameraPresent) return;
+
+ const toggle = document.getElementById('camera__toggle') as HTMLElement;
+ const cameraModal = document.getElementById(
+ 'camera-modal'
+ ) as HTMLDialogElement;
+ const cameraPreview = cameraModal.querySelector(
+ 'video'
+ ) as HTMLVideoElement;
+ const cameraClose = cameraModal.querySelector(
+ '.camera__close'
+ ) as HTMLElement;
+ const cameraShutter = cameraModal.querySelector(
+ '.camera__options__shutter'
+ ) as HTMLElement;
+
+ const picturePreviewContainer = document.getElementById(
+ 'chat__preview'
+ ) as HTMLElement;
+ const picturePreview = picturePreviewContainer.querySelector(
+ 'img'
+ ) as HTMLImageElement;
+ const removePreview = picturePreviewContainer.querySelector(
+ '.chat__preview__container__remove'
+ ) as HTMLElement;
+
+ this.camera.init(cameraPreview);
+
+ toggle.addEventListener('click', () => {
+ if (!cameraModal.open) {
+ this.camera.start();
+ (cameraModal as HTMLDialogElement).showModal();
+ }
+ });
+
+ cameraClose.addEventListener('click', () => {
+ cameraModal.close();
+ });
+
+ cameraModal.addEventListener('click', (e: MouseEvent) => {
+ this.closeModalOnClickOutside(e, cameraModal);
+ });
+
+ cameraModal.addEventListener('close', () => {
+ this.camera.stop();
+ });
+
+ cameraShutter.addEventListener('click', () => {
+ const snapShot = this.camera.takePicture();
+ if (!snapShot) return;
+ this.picture = snapShot;
+ picturePreview.src = this.picture;
+ cameraModal.close();
+ });
+
+ removePreview.addEventListener('click', () => {
+ picturePreview.removeAttribute('src');
+ this.picture = '';
+ });
+
+ console.log('[ChatPage] - successful setup Camera');
+ }
+
+ private closeModalOnClickOutside(
+ event: MouseEvent,
+ modal: HTMLDialogElement
+ ): void {
+ const modalBoundingBox = modal.getBoundingClientRect();
+
+ if (
+ event.clientX < modalBoundingBox.left ||
+ event.clientX > modalBoundingBox.right ||
+ event.clientY < modalBoundingBox.top ||
+ event.clientY > modalBoundingBox.bottom
+ ) {
+ modal.close();
+ }
+ }
+
+ private openImageInFullSize(imgSource: HTMLImageElement): void {
+ const imageModal = document.getElementById(
+ 'image-modal'
+ ) as HTMLDialogElement;
+ const previewElement = imageModal?.querySelector('img') as HTMLImageElement;
+ const imageClose = imageModal.querySelector('.image__close') as HTMLElement;
+
+ previewElement.src = imgSource.src;
+
+ if (imageModal.open) return;
+
+ imageModal.showModal();
+
+ imageModal.addEventListener('click', (e: MouseEvent) => {
+ this.closeModalOnClickOutside(e, imageModal);
+ });
+
+ imageClose.addEventListener('click', () => {
+ imageModal.close();
+ });
+ }
+
+ private setOptionsToggle(): void {
+ const optionsToggle = document.getElementById('options__toggle');
+
+ optionsToggle?.addEventListener('click', () => {
+ const optionsContainer = document.getElementById(
+ 'options'
+ ) as HTMLDialogElement;
+ optionsContainer?.classList.toggle('active');
+ });
+ console.log('[ChatPage] - activated options toggle');
+ }
+
+ private setupComposeMessage(): void {
+ this.setupSpeechToText();
+ this.setupCamera();
+
+ const composeContainer = document.getElementById(
+ 'chat__compose'
+ ) as HTMLDivElement;
+ const composeInput = composeContainer.querySelector(
+ 'textarea'
+ ) as HTMLTextAreaElement;
+ const composeSend = composeContainer.querySelector(
+ '.compose__send'
+ ) as HTMLElement;
+
+ composeInput.addEventListener('input', (e: any) => {
+ this.resizeTextarea(e.target);
+ });
+
+ composeSend.addEventListener('click', () => {
+ this.sendMessage();
+ });
+
+ console.log('[ChatPage] - successful setup compose message');
+ }
+
+ private resizeTextarea(srcElement: HTMLTextAreaElement): void {
+ const currentLineHeight = parseInt(
+ window.getComputedStyle(srcElement, null).getPropertyValue('line-height')
+ );
+
+ if (srcElement.scrollHeight < currentLineHeight * 5) {
+ srcElement.style.height = '0px';
+ srcElement.style.height = `${srcElement.scrollHeight}px`;
+ }
+ }
+
+ private async sendMessage(): Promise {
+ const composeContainer = document.getElementById(
+ 'chat__compose'
+ ) as HTMLDivElement;
+ const composeInput = composeContainer.querySelector(
+ 'textarea'
+ ) as HTMLTextAreaElement;
+
+ const picturePreviewContainer = document.getElementById(
+ 'chat__preview'
+ ) as HTMLElement;
+ const picturePreview = picturePreviewContainer.querySelector(
+ 'img'
+ ) as HTMLImageElement;
+
+ const message = composeInput.value.trim();
+
+ if (message === '' && this.picture === '') return;
+
+ let messageObject: SendMessage = {};
+ if (message !== '') {
+ messageObject.text = message;
+ }
+ if (this.picture !== '') {
+ messageObject.photo = this.picture.slice(22);
+ }
+
+ const successful = await this.messenger.sendMessage(messageObject);
+
+ if (!successful) return;
+
+ picturePreview.removeAttribute('src');
+ composeInput.value = '';
+ this.picture = '';
+
+ if (navigator.onLine) this.messenger.fetchMessages();
+ }
+
+ private setupFilterMessages(): void {
+ const search = document.getElementById('search');
+ const searchbar = document.getElementById('searchbar') as HTMLElement;
+ const searchInput = searchbar.querySelector('input') as HTMLInputElement;
+
+ if (search) {
+ search.addEventListener('click', () => {
+ if (!searchbar) return;
+
+ if (searchbar.classList.contains('active')) {
+ searchbar.classList.remove('active');
+ searchInput.value = '';
+ this.filterMessages('');
+ } else {
+ searchbar.classList.add('active');
+ }
+ });
+ }
+
+ if (searchbar) {
+ searchbar.addEventListener('input', (e: any) => {
+ this.filterMessages(e.target.value);
+ });
+ }
+ }
+
+ private filterMessages(query: string) {
+ const messages = document.getElementById('chat__window')?.children;
+
+ if (!messages) return;
+
+ this.resetFilterMessages(messages);
+ if (query === '') return;
+
+ if (query.match(/^@[a-zA-Z0-9]{8}$/)) {
+ console.log('filter by user id');
+ this.filterMessagesByUserId(query, messages);
+ return;
+ }
+
+ if (query.match(/^!img$/)) {
+ console.log('filter by image');
+ this.filterMessagesByImage(messages);
+ return;
+ }
+
+ this.filterMessagesByText(query, messages);
+ }
+
+ private resetFilterMessages(messages: HTMLCollection) {
+ for (let i = 0; i < messages.length; i++) {
+ const message = messages[i] as HTMLElement;
+ message.style.display = 'block';
+ }
+ }
+
+ private filterMessagesByText(query: string, messages: HTMLCollection) {
+ for (let i = 0; i < messages.length; i++) {
+ const message = messages[i] as HTMLElement;
+ const messageText = message.querySelector(
+ '.message__body__text__content'
+ ) as HTMLElement;
+
+ if (!messageText) continue;
+
+ if (messageText.innerText.includes(query)) {
+ message.style.display = 'block';
+ } else {
+ message.style.display = 'none';
+ }
+ }
+ }
+
+ private filterMessagesByImage(messages: HTMLCollection) {
+ for (let i = 0; i < messages.length; i++) {
+ const message = messages[i] as HTMLElement;
+ const messageImage = message.querySelector('img');
+
+ if (messageImage?.src !== '') {
+ console.log('FilterImage - show');
+ message.style.display = 'block';
+ } else {
+ console.log('FilterImage - hide');
+ message.style.display = 'none';
+ }
+ }
+ }
+
+ private filterMessagesByUserId(query: string, messages: HTMLCollection) {
+ query = query.slice(1);
+
+ for (let i = 0; i < messages.length; i++) {
+ const message = messages[i] as HTMLElement;
+ const messageUserId = message.dataset.sender;
+
+ if (messageUserId === query) {
+ message.style.display = 'block';
+ } else {
+ message.style.display = 'none';
+ }
+ }
+ }
+}
diff --git a/src/ts/pages/login.page.ts b/src/ts/pages/login.page.ts
new file mode 100644
index 0000000..36c4454
--- /dev/null
+++ b/src/ts/pages/login.page.ts
@@ -0,0 +1,186 @@
+import { LoginUser, RegisterUser } from '../_interface';
+import { Auth } from '../auth/auth';
+import { ROUTES } from '../common';
+import { Router } from '../router/router';
+
+export class LoginPage {
+ private static instance: LoginPage;
+ private auth: Auth;
+ private router: Router;
+
+ private constructor() {
+ this.auth = Auth.getInstance();
+ this.router = Router.getInstance();
+
+ document.addEventListener('spaContentLoaded', e => {
+ if ((e).detail.pageId === ROUTES['/login'].id) {
+ this.init();
+ }
+ });
+
+ console.log('[LoginPage] - constructed LoginPage');
+ }
+
+ public static getInstance(): LoginPage {
+ if (!LoginPage.instance) {
+ LoginPage.instance = new LoginPage();
+ }
+
+ return LoginPage.instance;
+ }
+
+ private init(): void {
+ this.setFormToggles();
+ this.setPasswordToggles();
+
+ this.setSignInEventlistener();
+ this.setSignUpEventlistener();
+
+ console.log('[LoginPage] - initialized LoginPage');
+ }
+
+ private setSignInEventlistener() {
+ const signInForm = document.getElementById('form__signin');
+
+ signInForm?.addEventListener('submit', (e: Event) => {
+ this.startSignInProcess(e);
+ e.preventDefault();
+ });
+ }
+
+ private async startSignInProcess(e: Event) {
+ const { user, stayLoggedIn } = this.getSignInFormData(
+ e.target as HTMLFormElement
+ );
+ //TODO: validate
+ const errors: boolean = this.validateSignIn(user);
+
+ if (!errors) return;
+
+ const success = await this.auth.login(user, stayLoggedIn);
+
+ if (success) {
+ this.router.navigate('/');
+ console.log('[LoginPage] - Login successful');
+ } else {
+ console.log('[LoginPage] - Login failed');
+ }
+ }
+
+ private getSignInFormData(form: HTMLFormElement): {
+ user: LoginUser;
+ stayLoggedIn: boolean;
+ } {
+ const formData = new FormData(form);
+
+ const user: LoginUser = {
+ userid: formData.get('userid') as string,
+ password: formData.get('password') as string
+ };
+
+ const stayLoggedIn: boolean = formData.get('stayloggedin') === 'on';
+
+ return { user, stayLoggedIn };
+ }
+
+ private validateSignIn(data: LoginUser): boolean {
+ let mistakes = 0;
+ // code
+ return true || mistakes > 0;
+ }
+
+ private setSignUpEventlistener() {
+ const signUpForm = document.getElementById('form__signup');
+
+ signUpForm?.addEventListener('submit', e => {
+ this.startSignUpProcess(e);
+ e.preventDefault();
+ });
+ }
+
+ private async startSignUpProcess(e: Event) {
+ const { user, passwordCheck } = this.getSignUpFormData(
+ e.target as HTMLFormElement
+ );
+ //TODO: validate
+ const error = this.validateSignUp(user, passwordCheck);
+
+ if (!error) return;
+
+ const success = await this.auth.register(user);
+
+ if (success) {
+ this.router.navigate('/');
+ console.log('[LoginPage] - SignUp successful');
+ } else {
+ console.log('[LoginPage] - SignUp failed');
+ }
+ }
+
+ private getSignUpFormData(form: HTMLFormElement): {
+ user: RegisterUser;
+ passwordCheck: string;
+ } {
+ const formData = new FormData(form);
+
+ const user: RegisterUser = {
+ userid: formData.get('userid') as string,
+ password: formData.get('password') as string,
+ nickname: formData.get('nickname') as string,
+ fullname: formData.get('fullname') as string
+ };
+
+ const passwordCheck: string = formData.get('confirmpassword') as string;
+
+ return { user, passwordCheck };
+ }
+
+ private validateSignUp(user: RegisterUser, passwordCheck: string): boolean {
+ return true;
+ // code
+ }
+
+ private setPasswordToggles() {
+ const passwordToggles = document.querySelectorAll(
+ '.form-content__password-toggle'
+ );
+
+ passwordToggles.forEach(toggle => {
+ toggle.addEventListener('click', (e: Event) => {
+ this.togglePassword(e);
+ });
+ });
+ }
+
+ private togglePassword(e: Event) {
+ const passwordIcon: HTMLElement = e.target as HTMLElement;
+ const passwordInput: HTMLInputElement =
+ passwordIcon.previousElementSibling as HTMLInputElement;
+
+ if (passwordInput.type === 'password') {
+ passwordInput.type = 'text';
+ passwordIcon.classList.toggle('show-password');
+ } else {
+ passwordInput.type = 'password';
+ passwordIcon.classList.toggle('show-password');
+ }
+ }
+
+ private setFormToggles() {
+ const formToggles = document.querySelectorAll('.forms__toggle');
+
+ formToggles.forEach(toggle => {
+ toggle.addEventListener('click', (e: Event) => {
+ this.toggleForm(e);
+ });
+ });
+ }
+
+ private toggleForm(e: Event) {
+ const container = document.getElementById('login__container');
+
+ container?.classList.toggle('signup-mode');
+
+ e.preventDefault();
+ }
+}
diff --git a/src/ts/router.ts b/src/ts/router.ts
deleted file mode 100644
index 6f927cb..0000000
--- a/src/ts/router.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { route } from './_interface/route.interface';
-import { Auth } from './auth';
-
-declare global {
- interface Window {
- route: Function;
- navigateTo: Function;
- }
-}
-
-export class Router {
- private routes: Object = {
- 404: {
- path: '/pages/404.html',
- title: '404',
- description: "The page you're looking for doesn't exist",
- loginRequired: false
- },
- '/': {
- path: '/pages/chat.html',
- title: 'Chat',
- description: 'Chat with your friends',
- loginRequired: true
- },
- '/login': {
- path: '/pages/login.html',
- title: 'Login',
- description: 'Login to your account',
- loginRequired: false
- },
- '/register': {
- path: '/pages/register.html',
- title: 'Register',
- description: 'Register a new account',
- loginRequired: false
- }
- };
- private spaContentLoadedEvent: Event;
- private auth: Auth;
-
- constructor() {
- this.auth = Auth.getInstance();
- this.spaContentLoadedEvent = new Event('spaContentLoaded');
-
- window.onpopstate = () => this.handleLocation;
- window.route = e => this.route(e);
- window.navigateTo = url => this.navigateTo(url);
-
- this.handleLocation();
- }
-
- public navigateTo(url: string): void {
- window.history.pushState({}, '', url);
- this.handleLocation();
- }
-
- public route(event): void {
- event = event || window.event;
- event.preventDefault();
- window.history.pushState({}, '', event.target.href);
- this.handleLocation();
- }
-
- public async handleLocation() {
- const path: string = window.location.pathname;
- const currentRoute: route =
- (!this.routes[path]?.loginRequired || this.auth.getActiveUser() != null
- ? this.routes[path] || this.routes[404]
- : this.routes['/login']) || this.routes[404];
-
- const html: string = await fetch(currentRoute.path).then(data =>
- data.text()
- );
- document.title = currentRoute.title + ' | PWA Chat';
- document
- .querySelector('meta[name="description"]')
- .setAttribute('content', currentRoute.description);
- document.body.innerHTML = html;
-
- document.body.dispatchEvent(this.spaContentLoadedEvent);
- }
-}
diff --git a/src/ts/router/router.ts b/src/ts/router/router.ts
new file mode 100644
index 0000000..e4575b4
--- /dev/null
+++ b/src/ts/router/router.ts
@@ -0,0 +1,77 @@
+import { Route } from '../_interface';
+import { Auth } from '../auth/auth';
+import { ROUTES } from '../common';
+
+export class Router {
+ private static instance: Router;
+ private auth: Auth;
+
+ private routeList: { [key: string]: Route } = ROUTES;
+
+ private constructor() {
+ this.auth = Auth.getInstance();
+
+ console.log('[Router] - instance created');
+ }
+
+ public static getInstance(): Router {
+ if (!Router.instance) {
+ Router.instance = new Router();
+ }
+
+ return this.instance;
+ }
+
+ public init(): void {
+ window.onpopstate = () => this.handleRoute();
+
+ this.handleRoute();
+
+ console.log('[Router] - initialized');
+ }
+
+ public navigate(path: string): void {
+ window.history.pushState({}, '', path);
+ this.handleRoute();
+ }
+
+ public route(event: any) {
+ event = event || window.event;
+ event.preventDefault();
+ window.history.pushState({}, '', event.target.href || '/');
+ this.handleRoute();
+ }
+
+ public async handleRoute() {
+ const path: string =
+ window.location.pathname in this.routeList
+ ? window.location.pathname
+ : '404';
+ const loggedIn: boolean = this.auth.getActiveUser() !== null;
+ const loginRequired: boolean = this.routeList[path].loginRequired;
+ const authenticatedForRoute: boolean = loginRequired && loggedIn;
+
+ let currentRoute: Route = authenticatedForRoute
+ ? this.routeList[path] || this.routeList['404']
+ : this.routeList['/login'] || this.routeList['404'];
+
+ if (loggedIn && currentRoute.id === 1) {
+ window.location.href = '/';
+ currentRoute = this.routeList['/'] || this.routeList['404'];
+ }
+
+ const pageContent = await fetch(currentRoute.path).then(data =>
+ data.text()
+ );
+
+ document.title = currentRoute.title + ' | PWA Chat';
+ document.body.innerHTML = pageContent;
+
+ const spaContentLoadedEvent = new CustomEvent('spaContentLoaded', {
+ detail: {
+ pageId: currentRoute.id
+ }
+ });
+ document.dispatchEvent(spaContentLoadedEvent);
+ }
+}
diff --git a/src/ts/util.ts b/src/ts/util.ts
new file mode 100644
index 0000000..e69de29
diff --git a/tsconfig.json b/tsconfig.json
index d92fe7a..330c2ae 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,15 +1,27 @@
{
"compilerOptions": {
- "module": "NodeNext",
- "moduleResolution": "classic",
+ "module": "esnext",
"target": "esnext",
+ "lib": [
+ "DOM",
+ "ESNext"
+ ],
+ "strict": true,
+ "allowSyntheticDefaultImports": true,
+ "moduleResolution": "node",
+ "esModuleInterop": true,
"sourceMap": true,
- "outDir": "src/dist",
+ "rootDir": "./src",
+ "outDir": "./dist",
},
"include": [
"src/**/*"
],
"exclude": [
- "node_modules"
+ "node_modules",
+ "assets",
+ "components",
+ "pages",
+ "sass"
]
}
\ No newline at end of file
diff --git a/webpack.common.cjs b/webpack.common.cjs
index 17ca5c4..e4f6d0c 100644
--- a/webpack.common.cjs
+++ b/webpack.common.cjs
@@ -33,7 +33,17 @@ module.exports = {
},
{
test: /\.sass$/,
- use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
+ use: [
+ MiniCssExtractPlugin.loader,
+ 'css-loader',
+ 'resolve-url-loader',
+ {
+ loader: 'sass-loader',
+ options: {
+ sourceMap: true
+ }
+ }
+ ],
exclude: /node_modules/
},
{