Skip to content

Commit 9e71be9

Browse files
authored
Merge pull request #977 from plone/volto-customisation-2025-weather-block
Update Weather Block for volto customisation training 2025
2 parents 82b2248 + 81d3497 commit 9e71be9

1 file changed

Lines changed: 257 additions & 28 deletions

File tree

docs/volto-customization/custom_block.md

Lines changed: 257 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ myst:
99

1010
# Volto Weather Block (custom block)
1111

12-
Let's create a Volto block that will display weather information for Brasilia. For this we can use [Open-Meteo API](https://open-meteo.com/). Open-Meteo is an open-source weather API and offers free access for non-commercial use. No API key required.
12+
Let's create a Volto block that will display weather information for Helsinki. For this we can use [Open-Meteo API](https://open-meteo.com/). Open-Meteo is an open-source weather API and offers free access for non-commercial use. No API key required.
1313

14-
Creating a basic block in Volto involves several steps. Below, I'll outline the steps to create a Volto block that displays the weather forecast in Brasilia.
14+
Creating a basic block in Volto involves several steps. Below, I'll outline the steps to create a Volto block that displays the weather forecast in Helsinki.
1515

1616
1. **Setup Your Volto Project:** If you haven't already, set up a Volto project. You can use the instructions presented in [Installation -> Bootstrap a new Volto project](installation.md) section.
1717

@@ -65,17 +65,31 @@ import React, { useEffect, useState } from "react";
6565

6666
const View = (props) => {
6767
const { data = {} } = props;
68-
const location = data.location || "Brasilia, Brazil";
68+
const location = data.location || "Helsinki, Finland";
6969

7070
const [weatherData, setWeatherData] = useState(null);
71+
const [temperatureData, setTemperatureData] = useState(null);
72+
73+
const getTemperatureColor = (temp) => {
74+
if (temp <= 0) return "#4B9FE1"; // Cold blue
75+
if (temp <= 10) return "#84CEF1"; // Cool blue
76+
if (temp <= 20) return "#F7B267"; // Warm orange
77+
return "#FF6B6B"; // Hot red
78+
};
79+
const getTemperatureHeight = (temp) => {
80+
// Normalize temperature to a reasonable bar height
81+
const baseHeight = 30; // minimum height
82+
const scale = 2; // multiplier for each degree
83+
return baseHeight + (temp + 10) * scale; // +10 to handle negative temps
84+
};
7185
useEffect(() => {
72-
const latitude = data.latitude || "-15.7797"; // Default latitude if no latitude is provided
73-
const longitude = data.longitude || "-47.9297"; // Default to longitude if no longitude is provided
86+
const latitude = data.latitude || "60.17"; // Default latitude if no latitude is provided
87+
const longitude = data.longitude || "24.94"; // Default to longitude if no longitude is provided
7488

7589
const abortController = new AbortController(); // creating an AbortController
7690

7791
fetch(
78-
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current_weather=true&timezone=auto`,
92+
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&hourly=precipitation_probability&forecast_days=1`,
7993
{ signal: abortController.signal } // passing the signal to the query
8094
)
8195
.then((response) => response.json())
@@ -88,6 +102,19 @@ const View = (props) => {
88102
throw error;
89103
});
90104

105+
fetch(
106+
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&hourly=temperature_2m&forecast_days=1`,
107+
{ signal: abortController.signal } // passing the signal to the query
108+
)
109+
.then((response) => response.json())
110+
.then((data) => {
111+
setTemperatureData(data);
112+
})
113+
.catch((error) => {
114+
if (error.name === "AbortError") return;
115+
console.error("Error fetching weather data:", error);
116+
throw error;
117+
});
91118
return () => {
92119
abortController.abort(); // stop the query by aborting on the AbortController on unmount
93120
};
@@ -96,9 +123,104 @@ const View = (props) => {
96123
return (
97124
<>
98125
{weatherData ? (
99-
<div>
100-
<h2>Weather in {location}</h2>
101-
<p>Temperature: {weatherData.current_weather.temperature} &deg;C</p>
126+
<div className="weather-block">
127+
<h2>Weather Forecast for {location}</h2>
128+
<div className="date">
129+
{new Date(weatherData?.hourly?.time[0]).toLocaleDateString(
130+
"en-US",
131+
{
132+
weekday: "long",
133+
year: "numeric",
134+
month: "long",
135+
day: "numeric",
136+
}
137+
)}
138+
</div>
139+
140+
{/* Temperature Section */}
141+
<h3>Temperature Forecast</h3>
142+
<div className="temperature-legend">
143+
<div className="legend-item">
144+
<span
145+
className="legend-color"
146+
style={{ backgroundColor: "#4B9FE1" }}
147+
></span>
148+
<span className="legend-text">Cold (≤ 0°C)</span>
149+
</div>
150+
<div className="legend-item">
151+
<span
152+
className="legend-color"
153+
style={{ backgroundColor: "#84CEF1" }}
154+
></span>
155+
<span className="legend-text">Cool (1-10°C)</span>
156+
</div>
157+
<div className="legend-item">
158+
<span
159+
className="legend-color"
160+
style={{ backgroundColor: "#F7B267" }}
161+
></span>
162+
<span className="legend-text">Warm (11-20°C)</span>
163+
</div>
164+
<div className="legend-item">
165+
<span
166+
className="legend-color"
167+
style={{ backgroundColor: "#FF6B6B" }}
168+
></span>
169+
<span className="legend-text">Hot (>20°C)</span>
170+
</div>
171+
</div>
172+
<div className="hourly-forecast temperature-forecast">
173+
{temperatureData?.hourly?.time.map((time, index) => {
174+
const hour = new Date(time).getHours();
175+
const temperature =
176+
temperatureData?.hourly?.temperature_2m[index];
177+
return (
178+
<div key={time} className="hourly-item">
179+
<div className="hour">{hour}:00</div>
180+
181+
<div className="temperature-container">
182+
<div
183+
className="temperature-bar"
184+
style={{
185+
height: `${getTemperatureHeight(temperature)}px`,
186+
backgroundColor: getTemperatureColor(temperature),
187+
}}
188+
>
189+
<span className="temperature-tooltip">
190+
{temperature.toFixed(1)}°C
191+
</span>
192+
</div>
193+
</div>
194+
</div>
195+
);
196+
})}
197+
</div>
198+
{/* Percipitation Section */}
199+
<h3>Precipitation Forecast</h3>
200+
<div className="hourly-forecast ">
201+
{weatherData?.hourly?.time.map((time, index) => {
202+
const hour = new Date(time).getHours();
203+
const probability =
204+
weatherData?.hourly?.precipitation_probability[index];
205+
return (
206+
<div key={time} className="hourly-item">
207+
<div className="hour">{hour}:00</div>
208+
<div className="probability">
209+
<div
210+
className="probability-bar"
211+
style={{
212+
height: `${probability}%`,
213+
backgroundColor: `rgba(0, 0, 255, ${
214+
probability / 100
215+
})`,
216+
}}
217+
/>
218+
</div>
219+
<div className="probability-value">{probability}%</div>
220+
</div>
221+
);
222+
})}
223+
</div>
102224
</div>
103225
) : (
104226
<p>Loading weather data...</p>
@@ -147,30 +269,128 @@ const Edit = (props) => {
147269
export default Edit;
148270
```
149271
150-
5. **Register the Block:** In your Volto project, locate the "components/index.js" file and add an the entries for your "Weather Block"
272+
5. **Add css for the Block:** In your Volto project , inside src add "theme" folder and create "weather.less" for adding style to your "Weather Block"
273+
274+
```less
275+
// Variables
276+
@primary-bg: #f5f5f5;
277+
@text-color: #666;
278+
@border-radius: 8px;
279+
280+
.weather-block {
281+
padding: 1rem;
282+
background: @primary-bg;
283+
border-radius: @border-radius;
284+
margin-bottom: 2rem;
285+
.date {
286+
font-size: 1.1rem;
287+
color: @text-color;
288+
margin-bottom: 1rem;
289+
font-style: italic;
290+
}
291+
292+
.hourly-forecast {
293+
display: flex;
294+
overflow-x: auto;
295+
padding: 1rem 0;
296+
flex-flow: wrap;
297+
gap: 1rem;
298+
height: 200px;
299+
align-items: flex-end;
300+
}
301+
302+
.hourly-item {
303+
display: flex;
304+
flex-direction: column;
305+
align-items: center;
306+
min-width: 40px;
307+
308+
.hour {
309+
font-size: 0.8rem;
310+
color: @text-color;
311+
margin-bottom: 0.5rem;
312+
}
313+
314+
.probability {
315+
height: 100px;
316+
width: 20px;
317+
background: #eee;
318+
border-radius: 10px;
319+
overflow: hidden;
320+
position: relative;
321+
322+
&-bar {
323+
position: absolute;
324+
bottom: 0;
325+
width: 100%;
326+
transition: height 0.3s ease;
327+
border-radius: 10px;
328+
}
329+
330+
&-value {
331+
font-size: 0.8rem;
332+
margin-top: 0.5rem;
333+
}
334+
}
335+
}
336+
337+
.temperature-legend {
338+
display: flex;
339+
gap: 20px;
340+
margin: 10px 0;
341+
flex-wrap: wrap;
342+
padding: 10px;
343+
background: @primary-bg;
344+
border-radius: 4px;
345+
346+
.legend-item {
347+
display: flex;
348+
align-items: center;
349+
gap: 8px;
350+
351+
.legend-color {
352+
width: 20px;
353+
height: 20px;
354+
border-radius: 4px;
355+
display: inline-block;
356+
}
357+
358+
.legend-text {
359+
font-size: 14px;
360+
color: @text-color;
361+
}
362+
}
363+
}
364+
365+
.legend {
366+
text-align: center;
367+
color: @text-color;
368+
margin-top: 1rem;
369+
font-size: 0.9rem;
370+
}
371+
}
372+
```
373+
374+
6. **Register the Block:** In your Volto project, add "components/Blocks/Weather/index.js" file and add an the entries for your "Weather Block" files and export them.
151375
152376
```js
153377
...
154378
import WeatherEdit from './components/Blocks/Weather/Edit';
155379
import WeatherView from './components/Blocks/Weather/View';
156380

157-
...
158381
export { WeatherView, WeatherEdit };
159-
160382
```
161383
162-
We need to configure the project to make it aware of a new block by adding it to the object configuration that is located in {file}`src/index.js`.
384+
We need to configure the project to make it aware of a new block by adding it to the object configuration that is located in {file}`src/config/blocks.ts`.
163385
For that we need the two blocks components we created and a SVG icon that will be displayed in the blocks chooser.
164386
165-
```js
166-
import WeatherEdit from './components/Blocks/Weather/Edit';
167-
import WeatherView from './components/Blocks/Weather/View';
387+
```ts
388+
import type { ConfigType } from '@plone/registry';
389+
import WeatherEdit from './../components/Blocks/Weather/Edit';
390+
import WeatherView from './../components/Blocks/Weather/View';
168391
import worldSVG from '@plone/volto/icons/world.svg';
169-
...
170-
export default function applyConfig(config) {
171-
172-
...
173392

393+
export default function install(config: ConfigType) {
174394
config.blocks.blocksConfig.weather = {
175395
id: 'weather',
176396
title: 'Weather',
@@ -181,20 +401,29 @@ export default function applyConfig(config) {
181401
restricted: false,
182402
mostUsed: false,
183403
sidebarTab: 1,
184-
blocks: {},
185-
security: {
186-
addPermission: [],
187-
view: [],
188-
},
189404
};
190405

191-
...
192-
193406
return config;
194407
};
195408
...
196409
```
197410
198-
6. **Use the Weather Block:** In Volto's Dexterity-based content types, create or edit a content type that includes the "Weather Block" in the allowedBlocks field. Then, create a content item and add the "Weather Block" to display the weather information for the location you specify.
411+
And then import the block config in {file}`src/index.ts` along with the css for the weather block.
412+
413+
```ts
414+
import type { ConfigType } from "@plone/registry";
415+
import installSettings from "./config/settings";
416+
import installBlocks from "./config/blocks";
417+
import "./theme/weather.less";
418+
function applyConfig(config: ConfigType) {
419+
installSettings(config);
420+
installBlocks(config);
421+
return config;
422+
}
423+
424+
export default applyConfig;
425+
```
426+
427+
7. **Use the Weather Block:** In Volto's Dexterity-based content types, create or edit a content type that includes the "Weather Block" in the allowedBlocks field. Then, create a content item and add the "Weather Block" to display the weather information for the location you specify.
199428
200429
Additionally, you may customize the UI and add more weather details based on the API's response data as needed.

0 commit comments

Comments
 (0)