Add images and wow styles
@@ -1,13 +1,19 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
|
|
||||||
import { PanelComponent } from '../components'
|
import { PanelComponent, PreviewComponent, ViewComponent } from '../components'
|
||||||
|
import places from '../assets'
|
||||||
|
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
|
|
||||||
|
const delay = () => new Promise(resolve => setTimeout(resolve, 800))
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [isLoading, setLoading] = useState(false)
|
||||||
const [isLeftPanelShown, setLeftPanelShown] = useState(false)
|
const [isLeftPanelShown, setLeftPanelShown] = useState(false)
|
||||||
const [isBottomPanelShown, setBottomPanelShown] = useState(true)
|
const [isBottomPanelShown, setBottomPanelShown] = useState(false)
|
||||||
|
const [activePlace, setActivePlace] = useState(0)
|
||||||
|
const [activeView, setActiveView] = useState(0)
|
||||||
|
|
||||||
const handleHideLeftPanel = useCallback(() => {
|
const handleHideLeftPanel = useCallback(() => {
|
||||||
setBottomPanelShown(false)
|
setBottomPanelShown(false)
|
||||||
@@ -19,18 +25,54 @@ export default function App() {
|
|||||||
setBottomPanelShown(!isBottomPanelShown)
|
setBottomPanelShown(!isBottomPanelShown)
|
||||||
}, [isBottomPanelShown])
|
}, [isBottomPanelShown])
|
||||||
|
|
||||||
|
const handleLeftPreviewClick = useCallback((value: number) => {
|
||||||
|
setActivePlace(value)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleBottomPreviewClick = (value: number) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setActiveView(value)
|
||||||
|
setLoading(true)
|
||||||
|
delay().then(() => {
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='main'>
|
<div className='main'>
|
||||||
|
<ViewComponent src={places[activePlace].view[activeView]}/>
|
||||||
<PanelComponent
|
<PanelComponent
|
||||||
orientation='left'
|
orientation='left'
|
||||||
isShown={isLeftPanelShown}
|
isShown={isLeftPanelShown}
|
||||||
setShown={handleHideLeftPanel}
|
setShown={handleHideLeftPanel}
|
||||||
/>
|
>
|
||||||
|
{places.map((place, index) => (
|
||||||
|
<PreviewComponent
|
||||||
|
isLoading={isLoading}
|
||||||
|
key={index}
|
||||||
|
value={index}
|
||||||
|
handleChange={handleLeftPreviewClick}
|
||||||
|
src={place.preview[0]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</PanelComponent>
|
||||||
<PanelComponent
|
<PanelComponent
|
||||||
orientation='bottom'
|
orientation='bottom'
|
||||||
isShown={isBottomPanelShown}
|
isShown={isBottomPanelShown}
|
||||||
setShown={handleHideBottomPanel}
|
setShown={handleHideBottomPanel}
|
||||||
/>
|
>
|
||||||
|
{places[activePlace].preview.map((preview, index) => (
|
||||||
|
<PreviewComponent
|
||||||
|
isLoading={isLoading}
|
||||||
|
key={index}
|
||||||
|
value={index}
|
||||||
|
handleChange={handleBottomPreviewClick}
|
||||||
|
src={preview}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</PanelComponent>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import preview from './preview'
|
||||||
|
import view from './view'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preview,
|
||||||
|
view
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import StormwindPark1 from './stormwind-park-1.jpg'
|
||||||
|
import StormwindPark2 from './stormwind-park-2.jpg'
|
||||||
|
import StormwindPark3 from './stormwind-park-3.jpg'
|
||||||
|
import StormwindPark4 from './stormwind-park-4.jpg'
|
||||||
|
import StormwindPark5 from './stormwind-park-5.jpg'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
StormwindPark1,
|
||||||
|
StormwindPark2,
|
||||||
|
StormwindPark3,
|
||||||
|
StormwindPark4,
|
||||||
|
StormwindPark5
|
||||||
|
]
|
||||||
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 55 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
import StormwindPark1 from './stormwind-park-1.jpg'
|
||||||
|
import StormwindPark2 from './stormwind-park-2.jpg'
|
||||||
|
import StormwindPark3 from './stormwind-park-3.jpg'
|
||||||
|
import StormwindPark4 from './stormwind-park-4.jpg'
|
||||||
|
import StormwindPark5 from './stormwind-park-5.jpg'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
StormwindPark1,
|
||||||
|
StormwindPark2,
|
||||||
|
StormwindPark3,
|
||||||
|
StormwindPark4,
|
||||||
|
StormwindPark5
|
||||||
|
]
|
||||||
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 491 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 498 KiB |
@@ -1,2 +1,3 @@
|
|||||||
export { ViewComponent } from './view-component/view-component'
|
export { ViewComponent } from './view-component/view-component'
|
||||||
export { PanelComponent } from './panel-component/panel-component'
|
export { PanelComponent } from './panel-component/panel-component'
|
||||||
|
export { PreviewComponent } from './preview-component/preview-component'
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
|
@import "../../app/style";
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
border: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 0;
|
height: 220px;
|
||||||
width: 0;
|
width: 360px;
|
||||||
|
background-image: $panelBackground;
|
||||||
|
background-repeat: repeat;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
z-index: 3;
|
||||||
|
cursor: pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
@@ -22,10 +32,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--bottom {
|
&--bottom {
|
||||||
bottom: 0;
|
bottom: -220px;
|
||||||
border: none;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: height 0.5s;
|
transition: bottom 0.5s;
|
||||||
|
|
||||||
|
.panel-border {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: -10px;
|
||||||
|
min-width: 100%;
|
||||||
|
background-image: $borderRight;
|
||||||
|
background-repeat: repeat;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -35,8 +54,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--shown {
|
&--shown {
|
||||||
border-top: 2px solid var(--foreground);
|
bottom: 0;
|
||||||
height: 360px;
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
height: 25px;
|
height: 25px;
|
||||||
@@ -46,10 +64,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--left {
|
&--left {
|
||||||
left: 0;
|
left: -360px;
|
||||||
border: none;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transition: width 0.5s;
|
transition: left 0.5s;
|
||||||
|
|
||||||
|
.panel-border {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
right: -8px;
|
||||||
|
min-height: 100%;
|
||||||
|
background-image: $borderTop;
|
||||||
|
background-repeat: repeat;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -59,8 +87,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--shown {
|
&--shown {
|
||||||
border-right: 2px solid var(--foreground);
|
left: 0;
|
||||||
width: 360px;
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
width: 25px;
|
width: 25px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import './panel-component.scss'
|
import './panel-component.scss'
|
||||||
|
|
||||||
@@ -7,9 +7,15 @@ interface Props {
|
|||||||
orientation: 'bottom' | 'left'
|
orientation: 'bottom' | 'left'
|
||||||
isShown: boolean
|
isShown: boolean
|
||||||
setShown: () => void
|
setShown: () => void
|
||||||
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PanelComponent = ({orientation, isShown, setShown}: Props) => {
|
export const PanelComponent = ({orientation, isShown, setShown, children}: Props) => {
|
||||||
|
const [isRendered, setRendered] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRendered(isShown)
|
||||||
|
}, [isShown])
|
||||||
|
|
||||||
const handleClick = useCallback((event) => {
|
const handleClick = useCallback((event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -20,6 +26,8 @@ export const PanelComponent = ({orientation, isShown, setShown}: Props) => {
|
|||||||
<div
|
<div
|
||||||
className={`panel panel--${orientation} ${isShown ? `panel--${orientation}--shown` : ''}`}
|
className={`panel panel--${orientation} ${isShown ? `panel--${orientation}--shown` : ''}`}
|
||||||
>
|
>
|
||||||
|
<div className='panel-border'/>
|
||||||
|
{isRendered && children}
|
||||||
<button onClick={handleClick}/>
|
<button onClick={handleClick}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
@import "../../app/style";
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
z-index: 4;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 180px;
|
||||||
|
margin: 5px;
|
||||||
|
border: 10px double var(--foreground);
|
||||||
|
border-image: $border 12 12 11 12;
|
||||||
|
border-image-width: 10px;
|
||||||
|
border-image-outset: 5px;
|
||||||
|
border-image-repeat: stretch stretch;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
transition: opacity 800ms ease-in-out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
import { Plug } from '../../assets'
|
||||||
|
import './preview-component.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isLoading: boolean
|
||||||
|
src: string
|
||||||
|
value: number
|
||||||
|
handleChange: (value: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PreviewComponent = ({src, value, handleChange, isLoading}: Props) => {
|
||||||
|
const [isLoaded, setLoaded] = useState(false)
|
||||||
|
const image = useMemo(() => {
|
||||||
|
setLoaded(false)
|
||||||
|
const img = new Image(320, 180)
|
||||||
|
img.src = src
|
||||||
|
img.onload = () => {
|
||||||
|
setLoaded(true)
|
||||||
|
}
|
||||||
|
return img
|
||||||
|
}, [src])
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
handleChange(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={handleClick}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${isLoaded ? image.src : Plug})`,
|
||||||
|
backgroundSize: `${isLoaded ? '320px 180px' : '90px 90px'}`,
|
||||||
|
opacity: `${isLoading ? '0.4' : '1'}`
|
||||||
|
}}
|
||||||
|
className='preview'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PreviewComponent.displayName = 'PreviewComponent'
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.view {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
min-width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-position: center;
|
||||||
|
transition: background-image 500ms ease-in;
|
||||||
|
animation: pulse 10s infinite;
|
||||||
|
animation-direction: alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,37 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
import { Background } from '../../assets'
|
||||||
|
|
||||||
|
import './view-component.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
src: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ViewComponent = ({src}: Props) => {
|
||||||
|
const [imageSrc, setImageSrc] = useState(Background)
|
||||||
|
const isImage = useMemo(() => imageSrc !== Background, [imageSrc])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const image = new Image()
|
||||||
|
image.src = src
|
||||||
|
image.onload = () => {
|
||||||
|
setImageSrc(src)
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
}, [src])
|
||||||
|
|
||||||
export const ViewComponent = () => {
|
|
||||||
return (
|
return (
|
||||||
<div/>
|
<div
|
||||||
|
className='view'
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${imageSrc})`,
|
||||||
|
backgroundRepeat: `${isImage ? 'no-repeat' : 'repeat'}`,
|
||||||
|
backgroundSize: `${isImage ? 'cover' : '256px'}`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
"downlevelIteration": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|||||||