diff --git a/backend/src/app.ts b/backend/src/app.ts index 7c53632..ba359bf 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -14,10 +14,12 @@ app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use('/dist', express.static(path.join(__dirname, '../../frontend/dist'))); -app.get('/', (req, res) => res.sendFile(path.join(__dirname, '../../frontend/dist/index.html'))); +app.get('/', (_, res) => res.sendFile(path.join(__dirname, '../../frontend/dist/index.html'))); app.use('/api', apiRouter); +app.use((_, res) => res.status(404).redirect('/')); + app.use(errorMiddleware); export default app; diff --git a/frontend/src/assets/images/guest.png b/frontend/src/assets/images/guest.png new file mode 100644 index 0000000..fd31bd3 Binary files /dev/null and b/frontend/src/assets/images/guest.png differ diff --git a/frontend/src/assets/images/login-background.png b/frontend/src/assets/images/login-background.png new file mode 100644 index 0000000..8203a98 Binary files /dev/null and b/frontend/src/assets/images/login-background.png differ diff --git a/frontend/src/assets/styles/colors.ts b/frontend/src/assets/styles/colors.ts index cb3ef1a..d18af31 100644 --- a/frontend/src/assets/styles/colors.ts +++ b/frontend/src/assets/styles/colors.ts @@ -1,5 +1,7 @@ const colors = { - primary: '#2AC1BC' + primary: '#2AC1BC', + offWhite: '#FCFCFC', + titleActive: '#1E2222' }; export default colors; diff --git a/frontend/src/ui-elements/cash-history/monthly-cash-history/index.ts b/frontend/src/ui-elements/cash-history/monthly-cash-history/index.ts index ec16224..79bd0ee 100644 --- a/frontend/src/ui-elements/cash-history/monthly-cash-history/index.ts +++ b/frontend/src/ui-elements/cash-history/monthly-cash-history/index.ts @@ -40,12 +40,14 @@ class MonthlyCashHistoryUIElement extends UIElement { return; } const $date = document.createElement('div'); + const { date, day, income, expenditure } = cashHistoriesInDay; + $date.innerHTML = `
-
${cashHistoriesInDay.month}월 ${cashHistoriesInDay.date}일
-
${getDayString(cashHistoriesInDay.day)}
-
수입 ${formatNumber(cashHistoriesInDay.income)}
-
지출 ${formatNumber(cashHistoriesInDay.expenditure)}
+
${cashHistoriesInDay.month}월 ${date}일
+
${getDayString(day)}
+ ${income > 0 ? `
수입 ${formatNumber(income)}
` : ''} + ${expenditure > 0 ? `
지출 ${formatNumber(expenditure)}
` : ''}
`; $container.appendChild($date); diff --git a/frontend/src/ui-elements/color-picker/index.css b/frontend/src/ui-elements/color-picker/index.css new file mode 100644 index 0000000..ef0265d --- /dev/null +++ b/frontend/src/ui-elements/color-picker/index.css @@ -0,0 +1,54 @@ +.color-picker { + width: 100%; + display: flex; + /* flex-direction: column; */ + align-items: center; + justify-content: space-between; + margin: 10px 0; +} + +.color-picker__palette { + display: flex; + align-items: center; + justify-content: space-between; + width: 45%; +} + +.color-picker__color { + width: 25px; + height: 25px; + border-radius: 8px; + cursor: pointer; +} + +.color-picker__input-wrapper { + display: flex; + align-items: center; + justify-content: center; +} + +.color-picker__color-hex { + background-color: var(--background); + width: 120px; + height: 30px; + border: 1px solid var(--line); + border-radius: 8px; + outline: none; +} + +.color-picker__current { + width: 25px; + height: 25px; + border-radius: 8px; +} + +.color-picker__refresh { + width: 25px; + height: 25px; + display: flex; + align-items: center; + justify-content: center; + margin-left: 10px; + font-size: var(--bold-medium); + font-weight: bold; +} \ No newline at end of file diff --git a/frontend/src/ui-elements/color-picker/index.ts b/frontend/src/ui-elements/color-picker/index.ts new file mode 100644 index 0000000..603931c --- /dev/null +++ b/frontend/src/ui-elements/color-picker/index.ts @@ -0,0 +1,129 @@ +import colors from '../../assets/styles/colors'; +import UIElement from '../../core/ui-element'; +import { generateRandomColor } from '../../utils/color'; + +import './index.css'; + +const COLOR_COUNT = 5; + +class ColorPickerUIElement extends UIElement { + private colors: string[] = []; + private $palette?: HTMLElement; + private $colorInput?: HTMLInputElement; + private $refreshButton?: HTMLElement; + + constructor ($target: HTMLElement) { + super($target, { + className: 'color-picker' + }); + } + + get value (): string | undefined { + return this.$colorInput?.value; + } + + clear (): void { + if (this.$colorInput !== undefined) { + this.$colorInput.value = ''; + } + } + + private assignColors () { + this.colors = []; + + for (let i = 0; i < COLOR_COUNT; i += 1) { + this.colors.push(generateRandomColor()); + } + } + + private renderPalette () { + if (this.$palette === undefined) { + return; + } + + this.$palette.innerHTML = this.colors.map((color) => { + return ` +
+ `; + }).join(''); + } + + protected render (): void { + this.assignColors(); + } + + private getFontColorByBackground (color: string): string | undefined { + const RGB_SUM_STD = 600; + + if (!color.match(/^#(?:[0-9a-f]{3}){1,2}$/i)) { + return; + } + + const r = parseInt(color.substring(1, 3), 16); + const g = parseInt(color.substring(3, 5), 16); + const b = parseInt(color.substring(5, 7), 16); + + if (r + g + b < RGB_SUM_STD) { + return colors.offWhite; + } + + return colors.titleActive; + } + + private setCurrentColor (color: string) { + if (this.$colorInput !== undefined && this.$refreshButton) { + this.$colorInput.value = color; + this.$refreshButton.style.backgroundColor = color; + const fontColor = this.getFontColorByBackground(color); + fontColor && this.$refreshButton.style.setProperty('color', fontColor); + } + } + + private onPaletteClicked (e: Event) { + const target = e.target as HTMLElement; + const { color } = target.dataset; + if (color === undefined) { + return; + } + + this.setCurrentColor(color); + } + + private onColorInput (e: Event) { + const { value } = e.target as HTMLInputElement; + this.setCurrentColor(value); + } + + private onRefreshClicked () { + this.assignColors(); + this.renderPalette(); + } + + protected addListener (): void { + this.$palette?.addEventListener('click', this.onPaletteClicked.bind(this)); + this.$colorInput?.addEventListener('input', this.onColorInput.bind(this)); + this.$refreshButton?.addEventListener('click', this.onRefreshClicked.bind(this)); + } + + protected mount (): void { + this.$palette = document.createElement('div'); + this.$palette.className = 'color-picker__palette'; + this.$element.appendChild(this.$palette); + this.renderPalette(); + + const $colorInputWrapper = document.createElement('div'); + $colorInputWrapper.className = 'color-picker__input-wrapper'; + this.$element.appendChild($colorInputWrapper); + + this.$colorInput = document.createElement('input'); + this.$colorInput.className = 'color-picker__color-hex'; + $colorInputWrapper.appendChild(this.$colorInput); + + this.$refreshButton = document.createElement('div'); + this.$refreshButton.className = 'color-picker__refresh color-picker__color'; + this.$refreshButton.innerHTML = 'RE'; + $colorInputWrapper.appendChild(this.$refreshButton); + } +} + +export default ColorPickerUIElement; diff --git a/frontend/src/ui-elements/input-modal/index.css b/frontend/src/ui-elements/input-modal/index.css index e319325..117cc1d 100644 --- a/frontend/src/ui-elements/input-modal/index.css +++ b/frontend/src/ui-elements/input-modal/index.css @@ -1,7 +1,39 @@ +@keyframes modal-fade-in { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes modal-fade-out { + from { + opacity: 1; + transform: translateY(0); + } + + to { + opacity: 0; + transform: translateY(20px); + } +} + .input-modal-wrapper.disappear { visibility: hidden; } +.input-modal-wrapper.appear .input-modal { + animation: modal-fade-in 0.3s; +} + +.input-modal-wrapper.disappear .input-modal { + animation: modal-fade-out 0.3s; +} + .input-modal-wrapper { position: fixed; width: 100vw; @@ -24,14 +56,16 @@ border-radius: 10px; display: flex; flex-direction: column; - padding: 36px; + padding: 28px 24px; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.1), 0px 4px 20px rgba(0, 0, 0, 0.1); + transition: all 0.3s; } .input-modal h1 { color: var(--title-active); } -.input-modal input { +.input-modal__input { border: 1px solid var(--line); padding: 12px; border-radius: 8px; @@ -51,4 +85,22 @@ align-items: center; margin-top: 40px; justify-content: space-between; +} + +.input-modal__button { + cursor: pointer; + font-size: var(--bold-medium); + color: var(--body); +} + +.input-modal__button:hover { + color: var(--label); +} + +.input-modal__button--confirm { + color: var(--primary3); +} + +.input-modal__button--confirm:hover { + color: var(--primary2); } \ No newline at end of file diff --git a/frontend/src/ui-elements/input-modal/index.ts b/frontend/src/ui-elements/input-modal/index.ts index d9ed63f..54c7248 100644 --- a/frontend/src/ui-elements/input-modal/index.ts +++ b/frontend/src/ui-elements/input-modal/index.ts @@ -1,4 +1,5 @@ import UIElement from '../../core/ui-element'; +import ColorPickerUIElement from '../color-picker'; import './index.css'; @@ -21,7 +22,7 @@ class InputModal extends UIElement { private $confirmButton?: HTMLElement; private $cancelButton?: HTMLElement; private $input?: HTMLInputElement; - private $colorInput?: HTMLInputElement; + private colorPicker?: ColorPickerUIElement; constructor ($target: HTMLElement, { title, @@ -45,18 +46,24 @@ class InputModal extends UIElement { } private onConfirmClicked () { - this.confirm(this.value ?? this.$input?.value ?? '', this.$colorInput?.value ?? ''); - this.close(); + this.confirm(this.value ?? this.$input?.value ?? '', this.colorPicker?.value ?? ''); } open (value?: string, label?: string): void { this.$element.classList.remove('disappear'); + this.$element.classList.add('appear'); this.value = value; label && this.$input?.setAttribute('value', label); } close (): void { + this.$element.classList.remove('appear'); this.$element.classList.add('disappear'); + if (this.$input !== undefined) { + this.$input.value = ''; + } + + this.colorPicker?.clear(); } private onCancelClicked () { @@ -84,10 +91,8 @@ class InputModal extends UIElement { $inputModal.appendChild(this.$input); if (this.hasColorPickerInput) { - this.$colorInput = document.createElement('input'); - this.$colorInput.className = 'input-modal__input'; - this.$colorInput.placeholder = '색상을 입력해주세요 (#ffffff)'; - $inputModal.appendChild(this.$colorInput); + this.colorPicker = new ColorPickerUIElement($inputModal); + this.colorPicker.build(); } const $buttonWrapper = document.createElement('div'); diff --git a/frontend/src/utils/color.ts b/frontend/src/utils/color.ts new file mode 100644 index 0000000..d7bdb24 --- /dev/null +++ b/frontend/src/utils/color.ts @@ -0,0 +1,3 @@ +export const generateRandomColor = (): string => { + return `#${(0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6)}`; +}; diff --git a/frontend/src/utils/toast/toast.css b/frontend/src/utils/toast/toast.css index 3ebc8d7..5a6ae49 100644 --- a/frontend/src/utils/toast/toast.css +++ b/frontend/src/utils/toast/toast.css @@ -1,8 +1,10 @@ #toast { - visibility: hidden; + z-index: 10; + opacity: 0; + transition: 0.4s ease-out; position: fixed; top: 40px; - right: 40px; + right: -300px; width: 300px; height: 65px; box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.1), 0px 4px 20px rgba(0, 0, 0, 0.1); @@ -10,30 +12,38 @@ display: flex; align-items: center; border-radius: 8px; + color: var(--body); } #toast.appear { - visibility: visible; + opacity: 1; + top: 40px; + right: 40px; } -#toast .toast__content { - padding: 0 12px; +.toast__content { + padding: 8px 16px; } -#toast h1 { - font-size: var(--body-medium); +.toast__title { + font-size: var(--body-regular); font-weight: bold; - margin-bottom: 10px; + margin-bottom: 4px; } -#toast .info h1 { +.toast__title.info { color: var(--primary1); } -#toast .success h1 { +.toast__title.success { color: var(--primary3); } -#toast .error h1 { +.toast__title.error { color: var(--error); +} + +.toast__message { + font-size: var(--body-regular); + } \ No newline at end of file diff --git a/frontend/src/utils/toast/toast.ts b/frontend/src/utils/toast/toast.ts index 1cb7c9c..a1daecd 100644 --- a/frontend/src/utils/toast/toast.ts +++ b/frontend/src/utils/toast/toast.ts @@ -1,7 +1,7 @@ import { $ } from '../selector'; import './toast.css'; -const DISAPPEAR_MS = 50000; +const DISAPPEAR_MS = 7000; type ToastTypeData = { title: string; @@ -40,9 +40,9 @@ const toast = () => { timer && clearTimeout(timer); $toast.innerHTML = ` -
-

${typeData.title}

- ${message} +
+
${typeData.title}
+
${message}
`; $toast.classList.add('appear'); diff --git a/frontend/src/view-models/console.ts b/frontend/src/view-models/console.ts index 262c20e..2accba1 100644 --- a/frontend/src/view-models/console.ts +++ b/frontend/src/view-models/console.ts @@ -34,7 +34,7 @@ class ConsoleViewModel extends ViewModel { private categoriesModel: CategoriesData; private paymentsModel: PaymentsData; private focusDateModel: FocusDateData; - private filteredCashHistoriesModel: CashHistoriesData; + private cashHistoriesModel: CashHistoriesData; private _cashHistoryType: CashHistories = CashHistories.Income; constructor (view: View) { @@ -43,7 +43,7 @@ class ConsoleViewModel extends ViewModel { this.categoriesModel = models.categories; this.paymentsModel = models.payments; this.focusDateModel = models.focusDate; - this.filteredCashHistoriesModel = models.filteredCashHistories; + this.cashHistoriesModel = models.cashHistories; this.initCashHistory(); this.fetchCategories(); @@ -88,7 +88,7 @@ class ConsoleViewModel extends ViewModel { const date = this.focusDateModel.focusDate; try { const histories = await cashHistoryAPI.fetchCashHistories(date.getFullYear(), date.getMonth() + 1); - this.filteredCashHistoriesModel.cashHistories = histories; + this.cashHistoriesModel.cashHistories = histories; } catch (error) { const { status } = error; @@ -127,6 +127,9 @@ class ConsoleViewModel extends ViewModel { async createPayment (value: string): Promise { try { await paymentAPI.createPayment(value); + toast.success('결제수단을 추가했습니다'); + (this.view as ConsoleView).closeCreatePaymentModal(); + this.fetchPayments(); } catch (error) { switch (error.status) { case 400: @@ -142,8 +145,6 @@ class ConsoleViewModel extends ViewModel { break; } } - - this.fetchPayments(); } async createCategory (value: string, color?: string): Promise { @@ -154,7 +155,11 @@ class ConsoleViewModel extends ViewModel { try { await categoryAPI.createCategory(value, color, this.cashHistoryType); + toast.success('카테고리를 추가했습니다'); + (this.view as ConsoleView).closeCreateCategoryModal(); + this.fetchCategories(); } catch (error) { + console.log('pass'); switch (error.status) { case 400: toast.error('양식을 확인해주세요'); @@ -169,15 +174,13 @@ class ConsoleViewModel extends ViewModel { break; } } - - this.fetchCategories(); } async deleteCategory (id: string): Promise { try { await categoryAPI.deleteCategory(Number(id)); - this.fetchCategories(); + toast.success('카테고리를 삭제했습니다'); } catch (error) { const { status } = error; @@ -190,8 +193,8 @@ class ConsoleViewModel extends ViewModel { async deletePayment (id: string): Promise { try { await paymentAPI.deletePayment(Number(id)); - this.fetchPayments(); + toast.success('결제수단을 삭제했습니다'); } catch (error) { const { status } = error; @@ -229,6 +232,7 @@ class ConsoleViewModel extends ViewModel { try { await cashHistoryAPI.createCashHistory(cashHistoryRequest); this.fetchCashHistories(); + toast.success('결제 내역을 추가했습니다'); } catch (error) { const { status } = error; @@ -246,6 +250,7 @@ class ConsoleViewModel extends ViewModel { try { await cashHistoryAPI.updateCashHistory(id, cashHistoryRequest); this.fetchCashHistories(); + toast.success('결제 내역을 수정했습니다'); } catch (error) { const { status } = error; diff --git a/frontend/src/view-models/main.ts b/frontend/src/view-models/main.ts index 9e4c5d9..088afc9 100644 --- a/frontend/src/view-models/main.ts +++ b/frontend/src/view-models/main.ts @@ -16,7 +16,7 @@ class MainViewModel extends ViewModel { private cashHistoriesModel: CashHistoriesData; private filteredCashHistoriesModel: CashHistoriesData; private cashHistoryModel: CashHistoryData; - private filterType: CashHistories | null; + private selectedFilter: CashHistories[] = [CashHistories.Income, CashHistories.Expenditure]; constructor (view: View) { super(view); @@ -24,30 +24,31 @@ class MainViewModel extends ViewModel { this.cashHistoriesModel = models.cashHistories; this.filteredCashHistoriesModel = models.filteredCashHistories; this.cashHistoryModel = models.cashHistory; - this.filterType = null; this.fetchCashHistories(); } protected subscribe (): void { - pubsub.subscribe(actions.ON_FOCUS_DATE_CHANGE, async () => { - await this.fetchCashHistories(); - - if (this.filterType === null) { - return; - } - this.filterData(this.filterType); + pubsub.subscribe(actions.ON_FOCUS_DATE_CHANGE, () => { + this.fetchCashHistories() + .then(() => { + this.applyFilter(); + }); }); pubsub.subscribe(actions.ON_CASH_HISTORIES_CHANGE, () => { - this.view.build(); + this.applyFilter(); }); pubsub.subscribe(actions.ON_FILTERED_CASH_HISTORIES_CHANGE, () => { this.view.build(); }); - pubsub.subscribe(actions.ON_CASH_HISTORY_CHANGE, () => { - this.view.build(); + pubsub.subscribe(actions.ON_CATEGORIES_CHANGE, () => { + this.fetchCashHistories(); + }); + + pubsub.subscribe(actions.ON_PAYMENTS_CHANGE, () => { + this.fetchCashHistories(); }); } @@ -57,7 +58,6 @@ class MainViewModel extends ViewModel { try { const histories = await cashHistoryAPI.fetchCashHistories(date.getFullYear(), date.getMonth() + 1); this.cashHistoriesModel.cashHistories = histories; - this.filteredCashHistoriesModel.cashHistories = histories; } catch (error) { const { status } = error; @@ -67,7 +67,7 @@ class MainViewModel extends ViewModel { } } - filterData (type: number): void { + applyFilter (): void { const { cashHistories } = this.cashHistoriesModel; if (cashHistories === null) { return; @@ -75,8 +75,8 @@ class MainViewModel extends ViewModel { const filtered = cashHistories.cashHistories.groupedCashHistories.map((monthlyCashHistory) => ({ ...monthlyCashHistory, - cashHistories: monthlyCashHistory.cashHistories.filter(e => e.type === type) - })); + cashHistories: monthlyCashHistory.cashHistories.filter(e => this.selectedFilter.includes(e.type)) + })).reverse(); this.filteredCashHistoriesModel.cashHistories = { ...cashHistories, @@ -88,27 +88,17 @@ class MainViewModel extends ViewModel { } filterButtonClick (isIncomeChecked: boolean, isExpenditureChecked: boolean): void { - if (isIncomeChecked && isExpenditureChecked) { - this.filteredCashHistoriesModel.cashHistories = this.cashHistoriesModel.cashHistories; - } else if (isIncomeChecked) { - this.filterType = CashHistories.Income; - this.filterData(this.filterType); - } else if (isExpenditureChecked) { - this.filterType = CashHistories.Expenditure; - this.filterData(this.filterType); - } else { - if (this.filteredCashHistoriesModel.cashHistories === null) { - return; - } - this.filteredCashHistoriesModel.cashHistories = { - ...this.filteredCashHistoriesModel.cashHistories, - cashHistories: { - totalIncome: 0, - totalExpenditure: 0, - groupedCashHistories: [] - } - }; + this.selectedFilter = []; + + if (isIncomeChecked) { + this.selectedFilter.push(CashHistories.Income); } + + if (isExpenditureChecked) { + this.selectedFilter.push(CashHistories.Expenditure); + } + + this.applyFilter(); } onCashHistoryClick (e:Event): void { diff --git a/frontend/src/views/category-expenditure/index.ts b/frontend/src/views/category-expenditure/index.ts index 8a23307..b32d782 100644 --- a/frontend/src/views/category-expenditure/index.ts +++ b/frontend/src/views/category-expenditure/index.ts @@ -119,7 +119,7 @@ class CategoryExpenditureView extends View { .reduce((acc, curr) => Math.min(acc, curr.price), max); const yLabels = new Array(5).fill(0) - .map((_, i) => `
${formatNumber(min + (max / 4) * i)} ₩
`) + .map((_, i) => `
${formatNumber(Math.round(min + (max / 4) * i))} ₩
`) .reverse() .join(''); diff --git a/frontend/src/views/console/index.ts b/frontend/src/views/console/index.ts index 94ad3b7..7f169e7 100644 --- a/frontend/src/views/console/index.ts +++ b/frontend/src/views/console/index.ts @@ -23,6 +23,14 @@ class ConsoleView extends View { this.consoleViewModel.createOrUpdate(); } + closeCreateCategoryModal (): void { + this.createCategoryModal?.close(); + } + + closeCreatePaymentModal (): void { + this.createPaymentModal?.close(); + } + enableButton (): void { $('.console__button')?.classList.add('console__button--active'); } diff --git a/frontend/src/views/login/index.css b/frontend/src/views/login/index.css index d01107e..caca58b 100644 --- a/frontend/src/views/login/index.css +++ b/frontend/src/views/login/index.css @@ -1,28 +1,34 @@ .login { --github-color: #171515; - --title-size: 32px; + font-size: var(--body-large); + display: flex; + width: 100vw; + height: 100vh; + overflow: hidden; +} +.login__left { + width: 400px; + height: 100%; + flex-grow: 1; display: flex; flex-direction: column; - align-items: center; + align-items: flex-end; justify-content: center; - margin-top: 100px; -} - -.login__title { - font-size: var(--title-size); - color: var(--title-active); - font-weight: bold; + padding-right: 8vw; + padding-bottom: 20vh; + background-color: var(--off-white); } .login__button { - margin-top: 20px; + z-index: 10; + margin-top: 30px; display: flex; align-items: center; justify-content: center; - width: 300px; - height: 40px; - border-radius: 3px; + width: 400px; + height: 60px; + border-radius: 4px; font-weight: bold; cursor: pointer; } @@ -40,4 +46,39 @@ .login__button--github:hover { --github-color: #474141; +} + +.login__button--guest { + background-color: var(--primary1); + color: var(--off-white); +} + +.login__button--guest > img { + background-color: var(--off-white); + border-radius: 50%; +} + +.login__button--guest:hover { + background-color: var(--primary2); +} + +.login__right { + width: 600px; + height: 100%; + flex-grow: 1; + display: flex; + align-items: center; + background: var(--primary1); +} + +.login__right img { + width: 90%; + padding-left: 5vw; + padding-bottom: 30vh; +} + +.ball { + position: absolute; + border-radius: 100%; + opacity: 0.3; } \ No newline at end of file diff --git a/frontend/src/views/login/index.ts b/frontend/src/views/login/index.ts index b2eb7a8..8c58150 100644 --- a/frontend/src/views/login/index.ts +++ b/frontend/src/views/login/index.ts @@ -2,6 +2,8 @@ import View from '../../core/view'; import { $ } from '../../utils/selector'; import LoginViewModel from '../../view-models/login'; import github from '../../assets/svg/github.svg'; +import guest from '../../assets/images/guest.png'; +import background from '../../assets/images/login-background.png'; import './index.css'; @@ -11,6 +13,49 @@ class LoginView extends View { constructor ($target: HTMLElement) { super($target); this.loginViewModel = new LoginViewModel(this); + this.createBackgroundBall(); + } + + createBackgroundBall (): void { + const colors = ['var(--primary1)', 'var(--primary2)', 'var(--off-white)', '#817DCE', '#4CA1DE']; + + const numBalls = 5; + const balls = []; + + for (let i = 0; i < numBalls; i++) { + const ball = document.createElement('div'); + ball.classList.add('ball'); + ball.style.background = colors[Math.floor(Math.random() * colors.length)]; + ball.style.left = `${Math.floor(Math.random() * 100)}vw`; + ball.style.top = `${Math.floor(Math.random() * 100)}vh`; + ball.style.transform = `scale(${Math.random()})`; + ball.style.width = `${Math.random() * 7}em`; + ball.style.height = ball.style.width; + + balls.push(ball); + document.body.append(ball); + } + + balls.forEach((el, i) => { + const to = { + x: Math.random() * (i % 2 === 0 ? -11 : 11), + y: Math.random() * 12 + }; + + el.animate( + [ + { transform: 'translate(0, 0)' }, + { transform: `translate(${to.x}rem, ${to.y}rem)` } + ], + { + duration: (Math.random() + 1) * 2000, // random duration + direction: 'alternate', + fill: 'both', + iterations: Infinity, + easing: 'ease-in-out' + } + ); + }); } protected addListener (): void { @@ -20,10 +65,19 @@ class LoginView extends View { protected render (): void { this.$target.innerHTML = `