Skip to content

Commit 7d6d56f

Browse files
authored
refactor Image component (#187)
Refactor Image component into a LumiComponent.
1 parent ede3f59 commit 7d6d56f

File tree

5 files changed

+353
-2
lines changed

5 files changed

+353
-2
lines changed

bower.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@
3030
"purescript-profunctor-lenses": ">=4.0.0 <7.0.0",
3131
"purescript-random": "^4.0.0",
3232
"purescript-react-basic": "^15.0.0",
33-
"purescript-react-basic-dom": "lumihq/purescript-react-basic-dom#^2.0.0",
33+
"purescript-react-basic-dom": "lumihq/purescript-react-basic-dom#^3.3.0",
3434
"purescript-react-basic-classic": "lumihq/purescript-react-basic-classic#^1.0.1",
3535
"purescript-react-basic-emotion": "^5.0.0",
3636
"purescript-react-basic-hooks": "^6.0.0",
37-
"purescript-react-dnd-basic": "^7.0.0",
37+
"purescript-react-dnd-basic": "^8.0.0",
3838
"purescript-record": ">= 1.0.0 < 3.0.0",
3939
"purescript-simple-json": ">=4.0.0 <7.0.0",
4040
"purescript-strings": "^4.0.0",

docs/App.jsx

+1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const componentv2Loaders = [
125125
"Button",
126126
"ButtonGroup",
127127
"Clip",
128+
"Image",
128129
"Link",
129130
"QRCode",
130131
"Slat",

docs/Examples2/Image.example.purs

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
module Lumi.Components2.Examples.Image where
2+
3+
import Prelude
4+
5+
import Data.Array as Array
6+
import Lumi.Components (($$$))
7+
import Lumi.Components.Example (example)
8+
import Lumi.Components.Spacing (Space(..), vspace)
9+
import Lumi.Components.Text (h4_)
10+
import Lumi.Components2.Image as Image
11+
import React.Basic.Classic (JSX)
12+
13+
docs :: JSX
14+
docs =
15+
let flexo = "https://s3.amazonaws.com/lumi-blog/avatars/_x600/flexo.jpg"
16+
in Array.intercalate (vspace S16)
17+
[ h4_ "Image default (will respect image's aspect ratio)"
18+
, example
19+
$ Image.image
20+
$$$ "http://via.placeholder.com/640x360"
21+
, h4_ "Image + resize { width: 120px, height: 40px }, respects image's aspect ratio & clips overflow"
22+
, example
23+
$ Image.image
24+
$ Image.resize { width: 120, height: 40 }
25+
$$$ "http://via.placeholder.com/640x360"
26+
, h4_ "Thumbnail default (will always have a square aspect ratio)"
27+
, example
28+
$ Image.thumbnail
29+
$$$ "http://via.placeholder.com/640x360"
30+
, h4_ "Thumbnail + resize 48px"
31+
, example
32+
$ Image.thumbnail
33+
$ Image.resizeSquare 80
34+
$$$ flexo
35+
, h4_ "Thumbnail + small"
36+
, example
37+
$ Image.thumbnail
38+
$ Image.small
39+
$$$ flexo
40+
, h4_ "Thumbnail + medium"
41+
, example
42+
$ Image.thumbnail
43+
$ Image.medium
44+
$$$ flexo
45+
, h4_ "Thumbnail + large"
46+
, example
47+
$ Image.thumbnail
48+
$ Image.large
49+
$$$ flexo
50+
, h4_ "Thumbnail + extra large"
51+
, example
52+
$ Image.thumbnail
53+
$ Image.extraLarge
54+
$$$ flexo
55+
, h4_ "Thumbnail + round"
56+
, example
57+
$ Image.thumbnail
58+
$ Image.round
59+
$ Image.extraLarge
60+
$$$ flexo
61+
, h4_ "Placeholders (can be overriden)"
62+
, example
63+
$ Image.image
64+
$ Image.resize { width: 900, height: 50 }
65+
$$$ ""
66+
, example
67+
$ Image.thumbnail
68+
$ Image.medium
69+
$$$ ""
70+
]

src/Lumi/Components/Svg.purs

+22
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,30 @@
11
module Lumi.Components.Svg where
22

3+
import Prelude
4+
5+
import Color (cssStringHSLA)
6+
import Lumi.Components.Color (colors)
37
import React.Basic.Classic (JSX)
48
import React.Basic.DOM.SVG as RS
59

10+
placeholderSvg :: JSX
11+
placeholderSvg =
12+
RS.svg
13+
{ viewBox: "0 0 24 24"
14+
, fill: "none"
15+
, stroke: cssStringHSLA colors.black4
16+
, xmlns: "http://www.w3.org/2000/svg"
17+
, children:
18+
[ "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
19+
]
20+
<#>
21+
{ d: _
22+
, strokeLinecap: "round"
23+
, strokeLinejoin: "round"
24+
}
25+
>>> RS.path
26+
}
27+
628
clientSvg :: JSX
729
clientSvg =
830
RS.svg

src/Lumi/Components2/Image.purs

+258
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
module Lumi.Components2.Image
2+
( image
3+
, thumbnail
4+
, small
5+
, medium
6+
, large
7+
, extraLarge
8+
, resize
9+
, resizeSquare
10+
, round
11+
, Image
12+
, Thumbnail
13+
) where
14+
15+
import Prelude
16+
17+
import Data.Foldable (minimum)
18+
import Data.Maybe (Maybe(..), fromMaybe, maybe)
19+
import Data.Monoid as Monoid
20+
import Data.Nullable as Nullable
21+
import Data.String as String
22+
import Data.Traversable (traverse)
23+
import Data.Tuple.Nested ((/\))
24+
import Effect.Timer (clearTimeout, setTimeout)
25+
import Effect.Unsafe (unsafePerformEffect)
26+
import Lumi.Components (LumiComponent, PropsModifier, lumiComponent, ($$$))
27+
import Lumi.Components.Loader (loader)
28+
import Lumi.Components.Svg (placeholderSvg)
29+
import Lumi.Components2.Box as Box
30+
import Lumi.Styles (Style, StyleModifier, style, style_, toCSS)
31+
import Lumi.Styles.Box (FlexAlign(..))
32+
import Lumi.Styles.Box as Styles.Box
33+
import Lumi.Styles.Slat hiding (_interactive,slat) as Styles.Slat
34+
import Lumi.Styles.Theme (LumiTheme(..), useTheme)
35+
import React.Basic.DOM as R
36+
import React.Basic.Emotion as E
37+
import React.Basic.Events (handler_)
38+
import React.Basic.Hooks (JSX)
39+
import React.Basic.Hooks as Hooks
40+
import Web.HTML.HTMLElement as HTMLElement
41+
42+
data Image = Image
43+
44+
type ImageProps
45+
= ( component :: Image
46+
, content :: String
47+
, placeholder :: Maybe JSX
48+
)
49+
50+
-- | An image has no size restrictions
51+
-- | will respect the image's original aspect ratio
52+
image :: LumiComponent ImageProps
53+
image =
54+
unsafePerformEffect do
55+
lumiComponent "Image" defaults \props -> Hooks.do
56+
theme <- useTheme
57+
loaded /\ setLoaded <- Hooks.useState' false
58+
59+
containerRef <- Hooks.useRef Nullable.null
60+
dimensions /\ setDimensions <- Hooks.useState { width: 20.0, height: 20.0 }
61+
Hooks.useLayoutEffect unit do
62+
container <- Hooks.readRefMaybe containerRef
63+
case container of
64+
Nothing -> mempty
65+
Just container' -> do
66+
rect <- traverse HTMLElement.getBoundingClientRect (HTMLElement.fromNode container')
67+
let
68+
newDimentions =
69+
{ height: maybe 0.0 (\r -> r.height + 2.0) rect
70+
, width: maybe 0.0 (\r -> r.width + 2.0) rect
71+
}
72+
if newDimentions /= dimensions then do
73+
timeout <-
74+
setTimeout 10 do
75+
setDimensions \_ -> newDimentions
76+
pure (clearTimeout timeout)
77+
else
78+
mempty
79+
80+
pure
81+
$ E.element R.div'
82+
{ ref: containerRef
83+
, children:
84+
[ if String.null props.content
85+
then fromMaybe placeholderSvg props.placeholder
86+
else
87+
Box.column
88+
$ Styles.Box._flex
89+
$ Styles.Box._align Center
90+
$ Styles.Box._justify Center
91+
$$$
92+
[ Monoid.guard (not loaded)
93+
$ let d = fromMaybe 20.0 ([ dimensions.height, dimensions.width ] # minimum)
94+
in loader
95+
{ style: R.css
96+
{ width: d * 0.25
97+
, height: d * 0.25
98+
, borderWidth: "2px"
99+
}
100+
, testId: Nullable.toNullable Nothing
101+
}
102+
, E.element R.img'
103+
{ src: props.content
104+
, className: ""
105+
, css: E.css { maxWidth: E.percent 100.0 }
106+
, onLoad: handler_ $ setLoaded true
107+
, onError: handler_ $ setLoaded true
108+
}
109+
]
110+
]
111+
, className: props.className
112+
, css: defaultImageStyle theme <> props.css theme
113+
}
114+
where
115+
defaults =
116+
{ component: Image
117+
, content: ""
118+
, placeholder: Nothing
119+
}
120+
121+
defaultImageStyle :: LumiTheme -> Style
122+
defaultImageStyle theme@(LumiTheme { colors }) =
123+
E.css
124+
{ boxSizing: E.borderBox
125+
, overflow: E.hidden
126+
, display: E.flex
127+
, flexFlow: E.column
128+
, alignItems: E.center
129+
, border: E.str "1px solid"
130+
, borderColor: E.color colors.black4
131+
}
132+
133+
134+
data Thumbnail = Thumbnail
135+
136+
type ThumbnailProps
137+
= ( component :: Thumbnail
138+
, content :: String
139+
, placeholder :: Maybe JSX
140+
)
141+
142+
-- | A thumbnail can support size restrictions
143+
-- | will always have a square aspect ratio
144+
thumbnail :: LumiComponent ThumbnailProps
145+
thumbnail =
146+
unsafePerformEffect do
147+
lumiComponent "Thumbnail" defaults \props -> Hooks.do
148+
pure
149+
$ image
150+
$ _ { content = props.content
151+
, placeholder = props.placeholder
152+
, css = toCSS defaultSize <> props.css
153+
}
154+
155+
where
156+
defaults =
157+
{ component: Thumbnail
158+
, content: ""
159+
, placeholder: Nothing
160+
}
161+
162+
-- | The `c` type parameter lets us constrain the type of component to which
163+
-- | an image modifier may be applied
164+
type ImageModifier c = forall r. PropsModifier (component :: c | r)
165+
166+
-- | restricted to only Image
167+
resize :: { width :: Int, height :: Int } -> ImageModifier Image
168+
resize props =
169+
style \(LumiTheme theme) ->
170+
E.css
171+
{ width: E.px props.width
172+
, height: E.px props.height
173+
}
174+
175+
-- | restricted to only Thumbnail
176+
round :: ImageModifier Thumbnail
177+
round = style_ mkRound
178+
179+
resizeSquare :: Int -> ImageModifier Thumbnail
180+
resizeSquare size =
181+
style \(LumiTheme theme) ->
182+
E.css
183+
{ width: E.px size
184+
, height: E.px size
185+
}
186+
187+
small :: ImageModifier Thumbnail
188+
small =
189+
style_
190+
$ E.css
191+
{ width: E.px 40
192+
, height: E.px 40
193+
}
194+
195+
medium :: ImageModifier Thumbnail
196+
medium = defaultSize
197+
198+
defaultSize :: StyleModifier
199+
defaultSize =
200+
style_ $ E.css
201+
{ width: E.px 56
202+
, height: E.px 56
203+
}
204+
205+
large :: ImageModifier Thumbnail
206+
large =
207+
style_
208+
$ E.css
209+
{ width: E.px 72
210+
, height: E.px 72
211+
}
212+
213+
extraLarge :: ImageModifier Thumbnail
214+
extraLarge =
215+
style_
216+
$ E.css
217+
{ width: E.px 140
218+
, height: E.px 140
219+
}
220+
221+
-- | we support two sizing scales for thumbnails
222+
-- | avatars use a smaller scale, whereas all other images use the above size scale
223+
smallAvatar :: ImageModifier Thumbnail
224+
smallAvatar =
225+
style_
226+
$ E.merge
227+
[ E.css
228+
{ width: E.px 24
229+
, height: E.px 24
230+
}
231+
, mkRound
232+
]
233+
234+
mediumAvatar :: ImageModifier Thumbnail
235+
mediumAvatar =
236+
style_
237+
$ E.merge
238+
[ E.css
239+
{ width: E.px 30
240+
, height: E.px 30
241+
}
242+
, mkRound
243+
]
244+
245+
largeAvatar :: ImageModifier Thumbnail
246+
largeAvatar =
247+
style_
248+
$ E.merge
249+
[ E.css
250+
{ width: E.px 36
251+
, height: E.px 36
252+
}
253+
, mkRound
254+
]
255+
256+
mkRound :: Style
257+
mkRound = E.css { borderRadius: E.percent 50.0 }
258+

0 commit comments

Comments
 (0)