diff --git a/.gitignore b/.gitignore index 11b581e..4f2e0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -138,4 +138,5 @@ dist # and uncomment the following lines # .pnp.* -# End of https://www.toptal.com/developers/gitignore/api/node,yarn \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/node,yarn +.vscode/settings.json diff --git a/index.html b/index.html index bcdc7c6..89d349f 100644 --- a/index.html +++ b/index.html @@ -10,5 +10,9 @@
+ diff --git a/src/App.js b/src/App.js index af630de..98c734c 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,10 @@ +import CarouselPage from './pages/CarouselPage.js' import ColorsPage from './pages/ColorsPage.js' +import CounterPage from './pages/CounterPage.js' +import DigitalClockPage from './pages/DigitalClockPage.js' import HexColorsGradientPage from './pages/HexColorsGradientPage.js' import HomePage from './pages/HomePage.js' +import MessagePage from './pages/MessagePage.js' import RandomQuotePage from './pages/RandomQuotePage.js' import RouterUtils from './utils/router.js' @@ -10,6 +14,10 @@ export default function App({ $target }) { const colorsPage = new ColorsPage({ $target }) const hexColorsGradientPage = new HexColorsGradientPage({ $target }) const randomQuotePage = new RandomQuotePage({ $target }) + const messagePage = new MessagePage({ $target }) + const counterPage = new CounterPage({ $target }) + const carouselPage = new CarouselPage({ $target }) + const digitalClockPage = new DigitalClockPage({ $target }) this.route = () => { const { pathname } = location @@ -23,6 +31,14 @@ export default function App({ $target }) { hexColorsGradientPage.render() } else if (pathname === '/random-quote') { randomQuotePage.render() + } else if (pathname === '/the-message') { + messagePage.render() + } else if (pathname === '/counter') { + counterPage.render() + } else if (pathname === '/image-carousel') { + carouselPage.render() + } else if (pathname === '/digital-clock') { + digitalClockPage.render() } } diff --git a/src/components/GenerateButton.js b/src/components/Button.js similarity index 68% rename from src/components/GenerateButton.js rename to src/components/Button.js index 87a94fb..b94db27 100644 --- a/src/components/GenerateButton.js +++ b/src/components/Button.js @@ -1,15 +1,15 @@ -export default function GenerateButton({ +export default function Button({ $target, text = 'Click Me!', className, - onClickGenerate, + onClick, }) { const $button = document.createElement('button') $button.className = `GenerateButton ${className}` - $button.textContent = text + $button.innerHTML = text $target.appendChild($button) $button.addEventListener('click', () => { - onClickGenerate() + onClick() }) } diff --git a/src/components/CountNumber.js b/src/components/CountNumber.js new file mode 100644 index 0000000..597be12 --- /dev/null +++ b/src/components/CountNumber.js @@ -0,0 +1,19 @@ +export default function CountNumber({ $target, initialState }) { + const $number = document.createElement('div') + $number.className = 'Counter__number' + + $target.appendChild($number) + + this.state = initialState + + this.setState = (nextState) => { + this.state = nextState + this.render() + } + + this.render = () => { + $number.textContent = this.state + } + + this.render() +} diff --git a/src/components/ImageCarousel.js b/src/components/ImageCarousel.js new file mode 100644 index 0000000..173ef7c --- /dev/null +++ b/src/components/ImageCarousel.js @@ -0,0 +1,53 @@ +import Button from './Button.js' + +export default function ImageCarousel({ + $target, + initialState, + onClickNext, + onClickPrev, +}) { + const $imageCarousel = document.createElement('div') + $imageCarousel.className = 'ImageCarousel' + + this.state = initialState + + $target.appendChild($imageCarousel) + + const $image = document.createElement('img') + $image.className = 'ImageCarousel__image' + + const $buttons = document.createElement('div') + $buttons.className = 'ImageCarousel__buttons' + + new Button({ + $target: $buttons, + text: '', + className: 'ImageCarousel__button', + onClick: onClickPrev, + }) + + new Button({ + $target: $buttons, + text: '', + className: 'ImageCarousel__button', + onClick: onClickNext, + }) + + this.setState = (nextState) => { + this.state = nextState + this.render() + } + + this.render = () => { + $imageCarousel.innerHTML = '' + + const { src, name } = this.state.image + $image.src = src + $image.alt = name + + $imageCarousel.appendChild($image) + $imageCarousel.appendChild($buttons) + } + + this.render() +} diff --git a/src/components/Indicators.js b/src/components/Indicators.js new file mode 100644 index 0000000..a5af2f3 --- /dev/null +++ b/src/components/Indicators.js @@ -0,0 +1,26 @@ +export default function Indicators({ $target, initialState, length }) { + const $indicators = document.createElement('div') + $indicators.className = 'Carousel__indicators' + $target.appendChild($indicators) + + this.state = initialState + + this.setState = (nextState) => { + this.state = nextState + this.render() + } + + this.render = () => { + const indicatorDots = Array.from(new Array(length), (_) => _) + .map( + (_, index) => ``, + ) + .join('') + $indicators.innerHTML = indicatorDots + } + + this.render() +} diff --git a/src/components/MessageForm.js b/src/components/MessageForm.js new file mode 100644 index 0000000..bc01e48 --- /dev/null +++ b/src/components/MessageForm.js @@ -0,0 +1,28 @@ +export default function MessageForm({ $target, onSubmit }) { + const $form = document.createElement('form') + + $target.appendChild($form) + + this.render = () => { + $form.innerHTML = ` +
+ + +
+ + ` + } + + $form.addEventListener('submit', (e) => { + e.preventDefault() + + const $input = $form.querySelector('.MessageForm__input') + onSubmit($input.value) + + $input.value = '' + }) + + this.render() +} diff --git a/src/components/MessagePreveiw.js b/src/components/MessagePreveiw.js new file mode 100644 index 0000000..66e5a98 --- /dev/null +++ b/src/components/MessagePreveiw.js @@ -0,0 +1,20 @@ +export default function MessagePreview({ $target, initialState }) { + const $preview = document.createElement('div') + $preview.className = 'MessageForm__preview' + + $target.appendChild($preview) + + this.state = initialState + + this.setState = (nextState) => { + this.state = nextState + this.render() + } + + this.render = () => { + $preview.style.display = this.state ? 'block' : 'none' + $preview.textContent = this.state + } + + this.render() +} diff --git a/src/pages/CarouselPage.js b/src/pages/CarouselPage.js new file mode 100644 index 0000000..73ca040 --- /dev/null +++ b/src/pages/CarouselPage.js @@ -0,0 +1,100 @@ +import { appendIfPageNotExists } from '../utils/render.js' +import ImageCarousel from '../components/ImageCarousel.js' +import Indicators from '../components/Indicators.js' + +const images = [ + { + name: 'Lam0', + src: 'https://iamcodefoxx.github.io/ImageCarousel/Lam0.jpg', + }, + { + name: 'Lam1', + src: 'https://iamcodefoxx.github.io/ImageCarousel/Lam1.jpg', + }, + { + name: 'Lam2', + src: 'https://iamcodefoxx.github.io/ImageCarousel/Lam2.jpg', + }, + { + name: 'Lam3', + src: 'https://iamcodefoxx.github.io/ImageCarousel/Lam3.jpg', + }, + { + name: 'Lam4', + src: 'https://iamcodefoxx.github.io/ImageCarousel/Lam4.jpg', + }, + { + name: 'Lam5', + src: 'https://iamcodefoxx.github.io/ImageCarousel/Lam5.jpg', + }, + { + name: 'Lam5', + src: 'https://iamcodefoxx.github.io/ImageCarousel/Lam6.jpg', + }, + { + name: 'Lam7', + src: 'https://iamcodefoxx.github.io/ImageCarousel/Lam7.jpg', + }, +] + +const IMAGES_LENGTH = images.length + +export default function CarouselPage({ $target }) { + const $page = document.createElement('div') + $page.className = 'CarouselPage' + + this.state = { + index: 0, + } + + const imageCarousel = new ImageCarousel({ + $target: $page, + initialState: { + index: this.state.index, + image: images[this.state.index], + }, + onClickNext: () => { + const index = (this.state.index + 1) % IMAGES_LENGTH + + this.setState({ + ...this.state, + index, + }) + }, + onClickPrev: () => { + const index = + this.state.index - 1 < 0 ? IMAGES_LENGTH - 1 : this.state.index - 1 + + this.setState({ + ...this.state, + index, + }) + }, + }) + + const indicators = new Indicators({ + $target: $page, + initialState: { + index: this.state.index, + }, + length: IMAGES_LENGTH, + }) + + this.setState = (nextState) => { + this.state = nextState + const { index } = this.state + + imageCarousel.setState({ + index, + image: images[index], + }) + + indicators.setState({ + index, + }) + } + + this.render = () => { + appendIfPageNotExists($target, $page) + } +} diff --git a/src/pages/ColorsPage.js b/src/pages/ColorsPage.js index 5a039c0..441b4d9 100644 --- a/src/pages/ColorsPage.js +++ b/src/pages/ColorsPage.js @@ -1,4 +1,4 @@ -import RandomColorButton from '../components/GenerateButton.js' +import RandomColorButton from '../components/Button.js' import { getRandomColor } from '../utils/colors.js' import { appendIfPageNotExists } from '../utils/render.js' @@ -19,7 +19,7 @@ export default function ColorsPage({ $target }) { $target: $page, text: 'Click Me!', className: 'RandomColorButton', - onClickGenerate: () => { + onClick: () => { const color = getRandomColor() this.setState({ color }) diff --git a/src/pages/CounterPage.js b/src/pages/CounterPage.js new file mode 100644 index 0000000..d415df2 --- /dev/null +++ b/src/pages/CounterPage.js @@ -0,0 +1,66 @@ +import Button from '../components/Button.js' +import CountNumber from '../components/CountNumber.js' +import { appendIfPageNotExists } from '../utils/render.js' + +const TITLE_TEXT = 'COUNTER' + +export default function CounterPage({ $target }) { + const $page = document.createElement('div') + $page.className = 'Counter' + + this.state = { + number: 0, + } + + this.setState = (nextState) => { + this.state = nextState + countNumber.setState(this.state.number) + } + + const $container = document.createElement('div') + $container.className = 'CounterContainer' + $container.innerHTML = ` +

${TITLE_TEXT}

+ ` + + const countNumber = new CountNumber({ + $target: $container, + initialState: this.state.number, + }) + + const $buttons = document.createElement('div') + $buttons.className = 'Counter__buttons' + $container.appendChild($buttons) + + new Button({ + $target: $buttons, + text: 'Increase', + className: 'Counter__button', + onClick: () => { + const number = this.state.number + 1 + this.setState({ + ...this.state, + number, + }) + }, + }) + + new Button({ + $target: $buttons, + text: 'Decrease', + className: 'Counter__button', + onClick: () => { + const number = this.state.number - 1 + this.setState({ + ...this.state, + number, + }) + }, + }) + + $page.appendChild($container) + + this.render = () => { + appendIfPageNotExists($target, $page) + } +} diff --git a/src/pages/DigitalClockPage.js b/src/pages/DigitalClockPage.js new file mode 100644 index 0000000..865a460 --- /dev/null +++ b/src/pages/DigitalClockPage.js @@ -0,0 +1,51 @@ +import { appendIfPageNotExists } from '../utils/render.js' +import { padDateNumber } from '../utils/date.js' + +const weekdays = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] + +export default function DigitalClockPage({ $target }) { + const $page = document.createElement('div') + $page.className = 'DigitalClock' + + this.state = { + interval: null, + } + + const $clock = document.createElement('div') + $clock.className = 'Clock' + + $page.appendChild($clock) + + this.render = () => { + appendIfPageNotExists($target, $page) + + this.state.interval = setInterval(() => { + const today = new Date() + const time = ` +
+ ${weekdays[today.getDay()]} +
+
+ ${padDateNumber(today.getHours() === 0 ? 12 : today.getHours())} +
+
:
+
+ ${padDateNumber(today.getHours())} +
+
:
+
+ ${padDateNumber(today.getSeconds())} +
+
+ ${today.getHours() >= 12 ? 'PM' : 'AM'} +
+ ` + + $clock.innerHTML = time + }, 1000) + } + + window.addEventListener('popstate', () => { + clearInterval(this.state.interval) + }) +} diff --git a/src/pages/HexColorsGradientPage.js b/src/pages/HexColorsGradientPage.js index 72adacb..027d315 100644 --- a/src/pages/HexColorsGradientPage.js +++ b/src/pages/HexColorsGradientPage.js @@ -1,4 +1,4 @@ -import GenerateButton from '../components/GenerateButton.js' +import GenerateButton from '../components/Button.js' import { getRandomColor } from '../utils/colors.js' import { appendIfPageNotExists } from '../utils/render.js' @@ -36,7 +36,7 @@ export default function HexColorsGradientPage({ $target }) { $target: $page, text: 'Click Me!', className: 'RandomGradientButton', - onClickGenerate: () => { + onClick: () => { const leftColor = getRandomColor() const rightColor = getRandomColor() diff --git a/src/pages/HomePage.js b/src/pages/HomePage.js index 11e63f4..a773667 100644 --- a/src/pages/HomePage.js +++ b/src/pages/HomePage.js @@ -13,6 +13,22 @@ const projects = [ name: 'Random Quote Generator', path: '/random-quote', }, + { + name: 'The message', + path: '/the-message', + }, + { + name: 'Counter', + path: '/counter', + }, + { + name: 'Image Carousel', + path: '/image-carousel', + }, + { + name: 'Digital Clock', + path: '/digital-clock', + }, ] export default function HomePage({ $target }) { diff --git a/src/pages/MessagePage.js b/src/pages/MessagePage.js new file mode 100644 index 0000000..8232614 --- /dev/null +++ b/src/pages/MessagePage.js @@ -0,0 +1,49 @@ +import MessageForm from '../components/MessageForm.js' +import MessagePreview from '../components/MessagePreveiw.js' +import { appendIfPageNotExists } from '../utils/render.js' + +const TITLE_TEXT = 'Pass the message' +const INFO_TEXT = 'Enter a message' + +export default function MessagePage({ $target }) { + const $page = document.createElement('div') + $page.className = 'Message' + + this.state = { + message: '', + } + + this.setState = (nextState) => { + this.state = nextState + messagePreview.setState(this.state.message) + } + + const $container = document.createElement('div') + $container.className = 'MessageForm' + $container.innerHTML = ` +

${TITLE_TEXT}

+
+

${INFO_TEXT}

+ ` + + new MessageForm({ + $target: $container, + onSubmit: (message) => { + this.setState({ + ...this.state, + message, + }) + }, + }) + + const messagePreview = new MessagePreview({ + $target: $container, + initialState: this.state.message, + }) + + $page.appendChild($container) + + this.render = () => { + appendIfPageNotExists($target, $page) + } +} diff --git a/src/pages/RandomQuotePage.js b/src/pages/RandomQuotePage.js index a7e96b9..cba0073 100644 --- a/src/pages/RandomQuotePage.js +++ b/src/pages/RandomQuotePage.js @@ -1,4 +1,4 @@ -import GenerateButton from '../components/GenerateButton.js' +import GenerateButton from '../components/Button.js' import { fetchQuote } from '../service/quoteApi.js' import { appendIfPageNotExists } from '../utils/render.js' @@ -14,7 +14,7 @@ export default function RandomQuotePage({ $target }) { $target: $page, text: 'Generate Quote', className: 'RandomQuote__button', - onClickGenerate: async () => { + onClick: async () => { if (this.state.isLoading) { return } diff --git a/src/styles/app.css b/src/styles/app.css index 94fdd8b..730bd16 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Noto+Sans&family=Dancing+Script&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans&family=Dancing+Script&family=Poiret+One&display=swap'); @keyframes changeColor { 0% { @@ -176,3 +176,267 @@ border-color: #117a8b; box-shadow: 0 0 0 4px rgba(58, 176, 195, 0.5); } + +/* project 4 */ +.Message { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + background-image: linear-gradient(#c3e0fc, white); +} + +.MessageForm { + display: flex; + flex-direction: column; + width: 80%; + max-width: 480px; + padding: 25px; + background-color: #f5f5f5; + border-radius: 5px; + box-shadow: 0px 2px 5px gray; +} + +.MessageForm__title { + font-size: 18px; + font-weight: 600; + text-align: center; +} + +.MessageForm__divider { + background-color: #d3d3d3; + height: 1px; + margin: 18px 0px; +} + +.MessageForm__info { + font-size: 14px; +} + +.MessageForm__input-area { + width: 100%; + display: flex; + margin: 20px 0px; +} + +.MessageForm__icon-button, +.MessageForm__input, +.MessageForm__submit { + outline: 0; + border: 1px solid #d3d3d3; + padding: 8px; +} + +.MessageForm__icon-button { + border-radius: 5px 0 0 5px; +} + +.MessageForm__icon-button:hover { + background-color: #d3d3d3; +} + +.MessageForm__input { + flex-grow: 1; + border-radius: 0 5px 5px 0; +} + +.MessageForm__preview { + padding: 20px 0px; + text-align: center; +} + +.MessageForm__submit { + border-radius: 5px; +} + +/* * project 5 */ +.Counter { + --page-bg-color: #fcfc62; + --btn-color: #feffea; + --btn-hover-color: #343a40; + --btn-box-shadow: rgba(52, 58, 64, 0.5) 0px 0px 0px 3.2px; +} + +.Counter { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + background-color: var(--page-bg-color); +} + +.CounterContainer { + width: 600px; + padding: 40px 0px; + background-color: var(--btn-color); + border: 4px solid; + border-radius: 5px; +} + +.Counter__title { + text-align: center; + font-size: 32px; +} + +.Counter__number { + font-size: 138px; + font-weight: 600; + text-align: center; + margin-top: 35px; +} + +.Counter__buttons { + display: flex; + justify-content: center; + margin-top: 28px; +} + +.Counter__button { + color: var(--btn-hover-color); + background-color: var(--btn-color); + margin-right: 6px; + padding: 6px 12px; +} + +.Counter__button:last-child { + margin-right: 0; +} + +.Counter__button:hover { + color: var(--btn-color); + background-color: var(--btn-hover-color); +} + +.Counter__button:focus { + box-shadow: var(--btn-box-shadow); +} + +/* Project 6 */ +@keyframes fadeIn { + from { + opacity: 0%; + } + to { + opacity: 100%; + } +} + +@media (max-width: 768px) { + .ImageCarousel { + position: relative; + width: 98%; + } +} + +@media (min-width: 768px) { + .ImageCarousel { + position: relative; + width: 70%; + } +} + +.CarouselPage { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.ImageCarousel__image { + width: 100%; + animation: fadeIn 0.8s ease-in; +} + +.ImageCarousel__buttons { + position: absolute; + top: 50%; + width: 100%; + display: flex; + justify-content: space-between; + transform: translateY(-50%); +} + +.ImageCarousel__button { + color: white; + background: transparent; + border-radius: 0px; + border: 0; +} + +.ImageCarousel__button:hover { + background-color: black; + opacity: 0.5; +} + +.ImageCarousel__button i { + pointer-events: none; +} +.Carousel__indicators { + display: flex; + margin-top: 10px; +} +.Carousel__indicator-dot { + width: 10px; + height: 10px; + background-color: gray; + border-radius: 50%; + margin-right: 8px; +} + +.Carousel__indicator-dot:last-child { + margin-right: 0px; +} + +.Carousel__indicator-dot--selected { + background-color: black; +} + +/* * project 7 */ +@media (max-width: 768px) { + .Clock__bar { + display: none; + } + + .Clock__number { + width: 100%; + } +} + +.DigitalClock { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-image: conic-gradient( + from 90deg at 25% -10%, + #ff4500, + #d3f340, + #7bee85, + #afeeee, + #7bee85 + ); +} + +.Clock { + display: flex; + justify-content: center; + flex-wrap: wrap; + width: 80%; + padding: 20px 0px; + background-color: rgba(0, 0, 0, 0.09); + color: white; + font-family: 'Poiret One'; + font-size: 5rem; + font-weight: 600; + text-align: center; +} + +.Clock * { + margin: 0 10px; + padding: 10px 0px; +} diff --git a/src/utils/date.js b/src/utils/date.js new file mode 100644 index 0000000..2e2b1d8 --- /dev/null +++ b/src/utils/date.js @@ -0,0 +1 @@ +export const padDateNumber = (number) => (number < 10 ? `0${number}` : number)