mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 17:26:26 +01:00
Merge pull request #1044 from ajnart/add-location-selection-for-weather-widget
✨ Improve location selection for weather
This commit is contained in:
@@ -1,389 +1,393 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"configProperties": {
|
||||
"name": "default"
|
||||
},
|
||||
"categories": [
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f",
|
||||
"position": 1,
|
||||
"name": "Welcome to Homarr 🎉",
|
||||
"type": "category"
|
||||
}
|
||||
],
|
||||
"wrappers": [
|
||||
{
|
||||
"id": "default",
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a326",
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"apps": [
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
|
||||
"name": "Discord",
|
||||
"url": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"isOpeningNewTab": true,
|
||||
"externalUrl": "https://discord.com/invite/aCsmEV5RgA"
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"statusCodes": [
|
||||
"200"
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/discord.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 3,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
|
||||
"name": "Donate",
|
||||
"url": "https://ko-fi.com/ajnart",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://ko-fi.com/ajnart",
|
||||
"externalUrl": "https://ko-fi.com/ajnart",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"statusCodes": [
|
||||
"200"
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/ko-fi.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 2
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 3,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330",
|
||||
"name": "Contribute",
|
||||
"url": "https://github.com/ajnart/homarr",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://github.com/ajnart/homarr",
|
||||
"externalUrl": "https://github.com/ajnart/homarr",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"statusCodes": []
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 2
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 4,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5df743d9-5cb1-457c-85d2-64ff86855652",
|
||||
"name": "Documentation",
|
||||
"url": "https://homarr.dev",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://homarr.dev",
|
||||
"externalUrl": "https://homarr.dev",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"statusCodes": [
|
||||
"200"
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "/imgs/logo/logo.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [
|
||||
{
|
||||
"id": "971aa859-8570-49a1-8d34-dd5c7b3638d1",
|
||||
"type": "date",
|
||||
"properties": {
|
||||
"display24HourFormat": true
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 4,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e3004052-6b83-480e-b458-56e8ccdca5f0",
|
||||
"type": "weather",
|
||||
"properties": {
|
||||
"displayInFahrenheit": false,
|
||||
"location": "Paris"
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 1,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"common": {
|
||||
"searchEngine": {
|
||||
"type": "google",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"layout": {
|
||||
"enabledLeftSidebar": false,
|
||||
"enabledRightSidebar": false,
|
||||
"enabledDocker": false,
|
||||
"enabledPing": false,
|
||||
"enabledSearchbar": true
|
||||
},
|
||||
"pageTitle": "Homarr v0.12 ⭐️",
|
||||
"logoImageUrl": "/imgs/logo/logo.png",
|
||||
"faviconUrl": "/imgs/favicon/favicon-squared.png",
|
||||
"backgroundImageUrl": "",
|
||||
"customCss": "",
|
||||
"colors": {
|
||||
"primary": "red",
|
||||
"secondary": "yellow",
|
||||
"shade": 7
|
||||
},
|
||||
"appOpacity": 100
|
||||
}
|
||||
"schemaVersion": 1,
|
||||
"configProperties": {
|
||||
"name": "default"
|
||||
},
|
||||
"categories": [
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f",
|
||||
"position": 1,
|
||||
"name": "Welcome to Homarr 🎉",
|
||||
"type": "category"
|
||||
}
|
||||
],
|
||||
"wrappers": [
|
||||
{
|
||||
"id": "default",
|
||||
"position": 0
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a326",
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"apps": [
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
|
||||
"name": "Discord",
|
||||
"url": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://discord.com/invite/aCsmEV5RgA",
|
||||
"isOpeningNewTab": true,
|
||||
"externalUrl": "https://discord.com/invite/aCsmEV5RgA"
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"statusCodes": [
|
||||
"200"
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/discord.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 3,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 3,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
|
||||
"name": "Donate",
|
||||
"url": "https://ko-fi.com/ajnart",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://ko-fi.com/ajnart",
|
||||
"externalUrl": "https://ko-fi.com/ajnart",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"statusCodes": [
|
||||
"200"
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/ko-fi.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 2
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 3,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330",
|
||||
"name": "Contribute",
|
||||
"url": "https://github.com/ajnart/homarr",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://github.com/ajnart/homarr",
|
||||
"externalUrl": "https://github.com/ajnart/homarr",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"statusCodes": []
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 2
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 4,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5df743d9-5cb1-457c-85d2-64ff86855652",
|
||||
"name": "Documentation",
|
||||
"url": "https://homarr.dev",
|
||||
"behaviour": {
|
||||
"onClickUrl": "https://homarr.dev",
|
||||
"externalUrl": "https://homarr.dev",
|
||||
"isOpeningNewTab": true
|
||||
},
|
||||
"network": {
|
||||
"enabledStatusChecker": false,
|
||||
"statusCodes": [
|
||||
"200"
|
||||
]
|
||||
},
|
||||
"appearance": {
|
||||
"iconUrl": "/imgs/logo/logo.png"
|
||||
},
|
||||
"integration": {
|
||||
"type": null,
|
||||
"properties": []
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 1,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"widgets": [
|
||||
{
|
||||
"id": "971aa859-8570-49a1-8d34-dd5c7b3638d1",
|
||||
"type": "date",
|
||||
"properties": {
|
||||
"display24HourFormat": true
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 4,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 2,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e3004052-6b83-480e-b458-56e8ccdca5f0",
|
||||
"type": "weather",
|
||||
"properties": {
|
||||
"displayInFahrenheit": false,
|
||||
"location": {
|
||||
"name": "Paris",
|
||||
"latitude": 48.85341,
|
||||
"longitude": 2.3488
|
||||
}
|
||||
},
|
||||
"area": {
|
||||
"type": "category",
|
||||
"properties": {
|
||||
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"md": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"sm": {
|
||||
"location": {
|
||||
"x": 1,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
},
|
||||
"lg": {
|
||||
"location": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 2,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"common": {
|
||||
"searchEngine": {
|
||||
"type": "google",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"layout": {
|
||||
"enabledLeftSidebar": false,
|
||||
"enabledRightSidebar": false,
|
||||
"enabledDocker": false,
|
||||
"enabledPing": false,
|
||||
"enabledSearchbar": true
|
||||
},
|
||||
"pageTitle": "Homarr v0.12 ⭐️",
|
||||
"logoImageUrl": "/imgs/logo/logo.png",
|
||||
"faviconUrl": "/imgs/favicon/favicon-squared.png",
|
||||
"backgroundImageUrl": "",
|
||||
"customCss": "",
|
||||
"colors": {
|
||||
"primary": "red",
|
||||
"secondary": "yellow",
|
||||
"shade": 7
|
||||
},
|
||||
"appOpacity": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
public/locales/en/widgets/location.json
Normal file
33
public/locales/en/widgets/location.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"form": {
|
||||
"field": {
|
||||
"query": "City / postal code",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude"
|
||||
},
|
||||
"button": {
|
||||
"search": {
|
||||
"label": "Search",
|
||||
"disabledTooltip": "Please choose a city / postal code first"
|
||||
}
|
||||
},
|
||||
"empty": "Unknown location"
|
||||
},
|
||||
"modal": {
|
||||
"title": "Choose a location",
|
||||
"table": {
|
||||
"header": {
|
||||
"city": "City",
|
||||
"country": "Country",
|
||||
"coordinates": "Coordinates",
|
||||
"population": "Population"
|
||||
},
|
||||
"action": {
|
||||
"select": "Select {{city}}, {{countryCode}}"
|
||||
},
|
||||
"population": {
|
||||
"fallback": "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import {
|
||||
Card,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Group,
|
||||
TextInput,
|
||||
Button,
|
||||
NumberInput,
|
||||
Modal,
|
||||
Table,
|
||||
Tooltip,
|
||||
ActionIcon,
|
||||
Loader,
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { IconListSearch, IconClick } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IntegrationOptionsValueType } from '../WidgetsEditModal';
|
||||
import { City } from '~/server/api/routers/weather';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
type LocationSelectionProps = {
|
||||
widgetId: string;
|
||||
propName: string;
|
||||
value: any;
|
||||
handleChange: (key: string, value: IntegrationOptionsValueType) => void;
|
||||
};
|
||||
|
||||
export const LocationSelection = ({
|
||||
widgetId,
|
||||
propName: key,
|
||||
value,
|
||||
handleChange,
|
||||
}: LocationSelectionProps) => {
|
||||
const { t } = useTranslation('widgets/location');
|
||||
const [query, setQuery] = useState(value.name ?? '');
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const selectionEnabled = query.length > 1;
|
||||
const emptyLocation = t('form.empty');
|
||||
|
||||
const onCitySelected = (city: City) => {
|
||||
close();
|
||||
handleChange(key, {
|
||||
name: city.name,
|
||||
latitude: city.latitude,
|
||||
longitude: city.longitude,
|
||||
});
|
||||
setQuery(city.name);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<Stack spacing="xs">
|
||||
<Title order={5}>{t(`modules/${widgetId}:descriptor.settings.${key}.label`)}</Title>
|
||||
|
||||
<Group noWrap align="end">
|
||||
<TextInput
|
||||
w="100%"
|
||||
label={t('form.field.query')}
|
||||
value={query}
|
||||
onChange={(ev) => {
|
||||
setQuery(ev.currentTarget.value);
|
||||
handleChange(key, {
|
||||
name: ev.currentTarget.value,
|
||||
longitude: '',
|
||||
latitude: '',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Tooltip hidden={selectionEnabled} label={t('form.button.search.disabledTooltip')}>
|
||||
<div>
|
||||
<Button
|
||||
disabled={!selectionEnabled}
|
||||
onClick={() => {
|
||||
if (selectionEnabled) open();
|
||||
}}
|
||||
variant="light"
|
||||
leftIcon={<IconListSearch size={16} />}
|
||||
>
|
||||
{t('form.button.search.label')}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Group grow>
|
||||
<NumberInput
|
||||
value={value.latitude}
|
||||
onChange={(inputValue) => {
|
||||
if (typeof inputValue !== 'number') return;
|
||||
handleChange(key, {
|
||||
...value,
|
||||
name: emptyLocation,
|
||||
latitude: inputValue,
|
||||
});
|
||||
setQuery(emptyLocation);
|
||||
}}
|
||||
precision={5}
|
||||
label={t('form.field.latitude')}
|
||||
hideControls
|
||||
/>
|
||||
<NumberInput
|
||||
value={value.longitude}
|
||||
onChange={(inputValue) => {
|
||||
if (typeof inputValue !== 'number') return;
|
||||
handleChange(key, {
|
||||
...value,
|
||||
name: emptyLocation,
|
||||
longitude: inputValue,
|
||||
});
|
||||
setQuery(emptyLocation);
|
||||
}}
|
||||
precision={5}
|
||||
label={t('form.field.longitude')}
|
||||
hideControls
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
<CitySelectModal
|
||||
opened={opened}
|
||||
closeModal={close}
|
||||
query={query}
|
||||
onCitySelected={onCitySelected}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type CitySelectModalProps = {
|
||||
opened: boolean;
|
||||
closeModal: () => void;
|
||||
query: string;
|
||||
onCitySelected: (location: City) => void;
|
||||
};
|
||||
|
||||
const CitySelectModal = ({ opened, closeModal, query, onCitySelected }: CitySelectModalProps) => {
|
||||
const { t } = useTranslation('widgets/location');
|
||||
const { isLoading, data } = api.weather.findCity.useQuery(
|
||||
{ query },
|
||||
{
|
||||
enabled: opened,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Title order={4}>
|
||||
{t('modal.title')} - {query}
|
||||
</Title>
|
||||
}
|
||||
size="xl"
|
||||
opened={opened}
|
||||
onClose={closeModal}
|
||||
zIndex={250}
|
||||
>
|
||||
<Stack>
|
||||
<Table striped>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '70%' }}>{t('modal.table.header.city')}</th>
|
||||
<th style={{ width: '50%' }}>{t('modal.table.header.country')}</th>
|
||||
<th>{t('modal.table.header.coordinates')}</th>
|
||||
<th>{t('modal.table.header.population')}</th>
|
||||
<th style={{ width: 40 }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading && (
|
||||
<tr>
|
||||
<td colSpan={5}>
|
||||
<Group position="center">
|
||||
<Loader />
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{data?.results.map((city) => (
|
||||
<tr key={city.id}>
|
||||
<td>
|
||||
<Text style={{ whiteSpace: 'nowrap' }}>{city.name}</Text>
|
||||
</td>
|
||||
<td>
|
||||
<Text style={{ whiteSpace: 'nowrap' }}>{city.country}</Text>
|
||||
</td>
|
||||
<td>
|
||||
<Text style={{ whiteSpace: 'nowrap' }}>
|
||||
{city.latitude}, {city.longitude}
|
||||
</Text>
|
||||
</td>
|
||||
<td>
|
||||
{city.population ? (
|
||||
<Text style={{ whiteSpace: 'nowrap' }}>{city.population}</Text>
|
||||
) : (
|
||||
<Text color="dimmed"> {t('modal.table.population.fallback')}</Text>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Tooltip
|
||||
label={t('modal.table.action.select', {
|
||||
city: city.name,
|
||||
countryCode: city.country_code,
|
||||
})}
|
||||
>
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="subtle"
|
||||
onClick={() => {
|
||||
onCitySelected(city);
|
||||
}}
|
||||
>
|
||||
<IconClick size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
<Group position="right">
|
||||
<Button variant="light" onClick={() => closeModal()}>
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import Widgets from '../../../../widgets';
|
||||
import type { IDraggableListInputValue, IWidgetOptionValue } from '../../../../widgets/widgets';
|
||||
import { IWidget } from '../../../../widgets/widgets';
|
||||
import { DraggableList } from './Inputs/DraggableList';
|
||||
import { LocationSelection } from './Inputs/LocationSelection';
|
||||
import { StaticDraggableList } from './Inputs/StaticDraggableList';
|
||||
|
||||
export type WidgetEditModalInnerProps = {
|
||||
@@ -35,7 +36,7 @@ export type WidgetEditModalInnerProps = {
|
||||
widgetOptions: IWidget<string, any>['properties'];
|
||||
};
|
||||
|
||||
type IntegrationOptionsValueType = IWidget<string, any>['properties'][string];
|
||||
export type IntegrationOptionsValueType = IWidget<string, any>['properties'][string];
|
||||
|
||||
export const WidgetsEditModal = ({
|
||||
context,
|
||||
@@ -200,6 +201,16 @@ const WidgetOptionTypeSwitch: FC<{
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
case 'location':
|
||||
return (
|
||||
<LocationSelection
|
||||
propName={key}
|
||||
value={value}
|
||||
handleChange={handleChange}
|
||||
widgetId={widgetId}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'draggable-list':
|
||||
/* eslint-disable no-case-declarations */
|
||||
const typedVal = value as IDraggableListInputValue['defaultValue'];
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function getServerSideProps({
|
||||
};
|
||||
}
|
||||
|
||||
const config = getFrontendConfig(configName as string);
|
||||
const config = await getFrontendConfig(configName as string);
|
||||
setCookie('config-name', configName, {
|
||||
req,
|
||||
res,
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function getServerSideProps({
|
||||
}
|
||||
|
||||
const translations = await getServerSideTranslations(dashboardNamespaces, locale, req, res);
|
||||
const config = getFrontendConfig(configName as string);
|
||||
const config = await getFrontendConfig(configName as string);
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { mediaServerRouter } from './routers/media-server';
|
||||
import { overseerrRouter } from './routers/overseerr';
|
||||
import { usenetRouter } from './routers/usenet/router';
|
||||
import { calendarRouter } from './routers/calendar';
|
||||
import { weatherRouter } from './routers/weather';
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@@ -32,6 +33,7 @@ export const rootRouter = createTRPCRouter({
|
||||
overseerr: overseerrRouter,
|
||||
usenet: usenetRouter,
|
||||
calendar: calendarRouter,
|
||||
weather: weatherRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
64
src/server/api/routers/weather.ts
Normal file
64
src/server/api/routers/weather.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { z } from 'zod';
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
const citySchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
country: z.string(),
|
||||
country_code: z.string(),
|
||||
latitude: z.number(),
|
||||
longitude: z.number(),
|
||||
population: z.number().optional(),
|
||||
});
|
||||
|
||||
const weatherSchema = z.object({
|
||||
current_weather: z.object({
|
||||
weathercode: z.number(),
|
||||
temperature: z.number(),
|
||||
}),
|
||||
daily: z.object({
|
||||
temperature_2m_max: z.array(z.number()),
|
||||
temperature_2m_min: z.array(z.number()),
|
||||
}),
|
||||
});
|
||||
|
||||
export const weatherRouter = createTRPCRouter({
|
||||
findCity: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
query: z.string().min(2),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
results: z.array(citySchema),
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => fetchCity(input.query)),
|
||||
at: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
longitude: z.number(),
|
||||
latitude: z.number(),
|
||||
})
|
||||
)
|
||||
.output(weatherSchema)
|
||||
.query(async ({ input }) => {
|
||||
const res = await fetch(
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=Europe%2FLondon`
|
||||
);
|
||||
return res.json();
|
||||
}),
|
||||
});
|
||||
|
||||
export type City = z.infer<typeof citySchema>;
|
||||
export type Weather = z.infer<typeof weatherSchema>;
|
||||
|
||||
const outputSchema = z.object({
|
||||
results: z.array(citySchema),
|
||||
});
|
||||
|
||||
export const fetchCity = async (query: string) => {
|
||||
const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${query}`);
|
||||
return outputSchema.parse(await res.json());
|
||||
};
|
||||
@@ -1,10 +1,19 @@
|
||||
import Consola from 'consola';
|
||||
|
||||
import { ConfigType } from '../../types/config';
|
||||
import fs from 'fs';
|
||||
import { BackendConfigType, ConfigType } from '../../types/config';
|
||||
import { getConfig } from './getConfig';
|
||||
import { fetchCity } from '~/server/api/routers/weather';
|
||||
|
||||
export const getFrontendConfig = (name: string): ConfigType => {
|
||||
const config = getConfig(name);
|
||||
export const getFrontendConfig = async (name: string): Promise<ConfigType> => {
|
||||
let config = getConfig(name);
|
||||
|
||||
const anyWeatherWidgetWithStringLocation = config.widgets.some(
|
||||
(widget) => widget.type === 'weather' && typeof widget.properties.location === 'string'
|
||||
);
|
||||
|
||||
if (anyWeatherWidgetWithStringLocation) {
|
||||
config = await migrateLocation(config);
|
||||
}
|
||||
|
||||
Consola.info(`Requested frontend content of configuration '${name}'`);
|
||||
// If not, return the config
|
||||
@@ -41,3 +50,39 @@ export const getFrontendConfig = (name: string): ConfigType => {
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const migrateLocation = async (config: BackendConfigType) => {
|
||||
Consola.log('Migrating config file to new location schema...', config.configProperties.name);
|
||||
|
||||
const configName = config.configProperties.name;
|
||||
const migratedConfig = {
|
||||
...config,
|
||||
widgets: await Promise.all(
|
||||
config.widgets.map(async (widget) =>
|
||||
widget.type !== 'weather' || typeof widget.properties.location !== 'string'
|
||||
? widget
|
||||
: {
|
||||
...widget,
|
||||
properties: {
|
||||
...widget.properties,
|
||||
location: await fetchCity(widget.properties.location)
|
||||
.then(({ results }) => ({
|
||||
name: results[0].name,
|
||||
latitude: results[0].latitude,
|
||||
longitude: results[0].longitude,
|
||||
}))
|
||||
.catch(() => ({
|
||||
name: '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
})),
|
||||
},
|
||||
}
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
fs.writeFileSync(`./data/configs/${configName}.json`, JSON.stringify(migratedConfig, null, 2));
|
||||
|
||||
return migratedConfig;
|
||||
};
|
||||
|
||||
@@ -2,13 +2,11 @@ import Consola from 'consola';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Config, serviceItem } from '../types';
|
||||
import { ConfigAppIntegrationType, ConfigAppType, IntegrationType } from '../../types/app';
|
||||
import { AreaType } from '../../types/area';
|
||||
import { CategoryType } from '../../types/category';
|
||||
import { BackendConfigType } from '../../types/config';
|
||||
import { SearchEngineCommonSettingsType } from '../../types/settings';
|
||||
import { IWidget } from '../../widgets/widgets';
|
||||
import { ICalendarWidget } from '../../widgets/calendar/CalendarTile';
|
||||
import { IDashDotTile } from '../../widgets/dashDot/DashDotTile';
|
||||
import { IDateWidget } from '../../widgets/date/DateTile';
|
||||
@@ -16,6 +14,8 @@ import { ITorrentNetworkTraffic } from '../../widgets/download-speed/TorrentNetw
|
||||
import { ITorrent } from '../../widgets/torrent/TorrentTile';
|
||||
import { IUsenetWidget } from '../../widgets/useNet/UseNetTile';
|
||||
import { IWeatherWidget } from '../../widgets/weather/WeatherTile';
|
||||
import { IWidget } from '../../widgets/widgets';
|
||||
import { Config, serviceItem } from '../types';
|
||||
|
||||
export function migrateConfig(config: Config): BackendConfigType {
|
||||
const newConfig: BackendConfigType = {
|
||||
@@ -208,7 +208,11 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
|
||||
type: 'weather',
|
||||
properties: {
|
||||
displayInFahrenheit: oldModule.options?.freedomunit?.value ?? false,
|
||||
location: oldModule.options?.location?.value ?? 'Paris',
|
||||
location: {
|
||||
name: oldModule.options?.location?.value ?? '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
},
|
||||
},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const dashboardNamespaces = [
|
||||
'modules/bookmark',
|
||||
'widgets/error-boundary',
|
||||
'widgets/draggable-list',
|
||||
'widgets/location',
|
||||
];
|
||||
|
||||
export const loginNamespaces = ['authentication/login'];
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Center, Group, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useElementSize } from '@mantine/hooks';
|
||||
import { IconArrowDownRight, IconArrowUpRight, IconCloudRain } from '@tabler/icons-react';
|
||||
import { api } from '~/utils/api';
|
||||
import { defineWidget } from '../helper';
|
||||
import { IWidget } from '../widgets';
|
||||
import { useWeatherForCity } from './useWeatherForCity';
|
||||
import { WeatherIcon } from './WeatherIcon';
|
||||
|
||||
const definition = defineWidget({
|
||||
@@ -15,8 +15,12 @@ const definition = defineWidget({
|
||||
defaultValue: false,
|
||||
},
|
||||
location: {
|
||||
type: 'text',
|
||||
defaultValue: 'Paris',
|
||||
type: 'location',
|
||||
defaultValue: {
|
||||
name: 'Paris',
|
||||
latitude: 48.85341,
|
||||
longitude: 2.3488,
|
||||
},
|
||||
},
|
||||
},
|
||||
gridstack: {
|
||||
@@ -35,8 +39,8 @@ interface WeatherTileProps {
|
||||
}
|
||||
|
||||
function WeatherTile({ widget }: WeatherTileProps) {
|
||||
const { data: weather, isLoading, isError } = useWeatherForCity(widget.properties.location);
|
||||
const { width, height, ref } = useElementSize();
|
||||
const { data: weather, isLoading, isError } = api.weather.at.useQuery(widget.properties.location);
|
||||
const { width, ref } = useElementSize();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -77,10 +81,10 @@ function WeatherTile({ widget }: WeatherTileProps) {
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<Group align="center" position="center" spacing="xs">
|
||||
<WeatherIcon code={weather!.current_weather.weathercode} />
|
||||
<WeatherIcon code={weather.current_weather.weathercode} />
|
||||
<Title>
|
||||
{getPerferedUnit(
|
||||
weather!.current_weather.temperature,
|
||||
weather.current_weather.temperature,
|
||||
widget.properties.displayInFahrenheit
|
||||
)}
|
||||
</Title>
|
||||
@@ -89,12 +93,12 @@ function WeatherTile({ widget }: WeatherTileProps) {
|
||||
<Group noWrap spacing="xs">
|
||||
<IconArrowUpRight />
|
||||
{getPerferedUnit(
|
||||
weather!.daily.temperature_2m_max[0],
|
||||
weather.daily.temperature_2m_max[0],
|
||||
widget.properties.displayInFahrenheit
|
||||
)}
|
||||
<IconArrowDownRight />
|
||||
{getPerferedUnit(
|
||||
weather!.daily.temperature_2m_min[0],
|
||||
weather.daily.temperature_2m_min[0],
|
||||
widget.properties.displayInFahrenheit
|
||||
)}
|
||||
</Group>
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// To parse this data:
|
||||
//
|
||||
// import { Convert, WeatherResponse } from "./file";
|
||||
//
|
||||
// const weatherResponse = Convert.toWeatherResponse(json);
|
||||
//
|
||||
// These functions will throw an error if the JSON doesn't
|
||||
// match the expected interface, even if the JSON is valid.
|
||||
|
||||
export interface WeatherResponse {
|
||||
current_weather: CurrentWeather;
|
||||
utc_offset_seconds: number;
|
||||
latitude: number;
|
||||
elevation: number;
|
||||
longitude: number;
|
||||
generationtime_ms: number;
|
||||
daily_units: DailyUnits;
|
||||
daily: Daily;
|
||||
}
|
||||
|
||||
export interface CurrentWeather {
|
||||
winddirection: number;
|
||||
windspeed: number;
|
||||
time: string;
|
||||
weathercode: number;
|
||||
temperature: number;
|
||||
}
|
||||
|
||||
export interface Daily {
|
||||
temperature_2m_max: number[];
|
||||
time: Date[];
|
||||
temperature_2m_min: number[];
|
||||
weathercode: number[];
|
||||
}
|
||||
|
||||
export interface DailyUnits {
|
||||
temperature_2m_max: string;
|
||||
temperature_2m_min: string;
|
||||
time: string;
|
||||
weathercode: string;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { WeatherResponse } from './types';
|
||||
|
||||
/**
|
||||
* Requests the weather of the specified city
|
||||
* @param cityName name of the city where the weather should be requested
|
||||
* @returns weather of specified city
|
||||
*/
|
||||
export const useWeatherForCity = (cityName: string) => {
|
||||
const {
|
||||
data: city,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ['weatherCity', { cityName }],
|
||||
queryFn: () => fetchCity(cityName),
|
||||
cacheTime: 1000 * 60 * 60 * 24, // the city is cached for 24 hours
|
||||
staleTime: Infinity, // the city is never considered stale
|
||||
});
|
||||
const weatherQuery = useQuery({
|
||||
queryKey: ['weather', { cityName }],
|
||||
queryFn: () => fetchWeather(city?.results[0]),
|
||||
enabled: Boolean(city),
|
||||
cacheTime: 1000 * 60 * 60 * 6, // the weather is cached for 6 hours
|
||||
staleTime: 1000 * 60 * 5, // the weather is considered stale after 5 minutes
|
||||
});
|
||||
|
||||
return {
|
||||
...weatherQuery,
|
||||
isLoading: weatherQuery.isLoading || isLoading,
|
||||
isError: weatherQuery.isError || isError,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests the coordinates of a city
|
||||
* @param cityName name of city
|
||||
* @returns list with all coordinates for citites with specified name
|
||||
*/
|
||||
const fetchCity = async (cityName: string) => {
|
||||
const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${cityName}`);
|
||||
return (await res.json()) as { results: Coordinates[] };
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests the weather of specific coordinates
|
||||
* @param coordinates of the location the weather should be fetched
|
||||
* @returns weather of specified coordinates
|
||||
*/
|
||||
async function fetchWeather(coordinates?: Coordinates) {
|
||||
if (!coordinates) return null;
|
||||
const { longitude, latitude } = coordinates;
|
||||
const res = await fetch(
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=Europe%2FLondon`
|
||||
);
|
||||
// eslint-disable-next-line consistent-return
|
||||
return (await res.json()) as WeatherResponse;
|
||||
}
|
||||
|
||||
type Coordinates = { latitude: number; longitude: number };
|
||||
@@ -40,7 +40,8 @@ export type IWidgetOptionValue =
|
||||
| INumberInputOptionValue
|
||||
| IDraggableListInputValue
|
||||
| IDraggableEditableListInputValue<any>
|
||||
| IMultipleTextInputOptionValue;
|
||||
| IMultipleTextInputOptionValue
|
||||
| ILocationOptionValue;
|
||||
|
||||
// Interface for data type
|
||||
interface DataType {
|
||||
@@ -95,6 +96,11 @@ export type ISliderInputOptionValue = {
|
||||
inputProps?: Partial<SliderProps>;
|
||||
};
|
||||
|
||||
type ILocationOptionValue = {
|
||||
type: 'location';
|
||||
defaultValue: { latitude: number; longitude: number };
|
||||
};
|
||||
|
||||
// will show a sortable list that can have sub settings
|
||||
export type IDraggableListInputValue = {
|
||||
type: 'draggable-list';
|
||||
|
||||
Reference in New Issue
Block a user