Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions edge-apps/weather/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The app accepts the following settings via `screenly.yml`:

| Setting | Description | Type | Default |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------- |
| `display_errors` | For debugging purposes to display errors on the screen. | optional, advanced | `false` |
| `openweathermap_api_key` | OpenWeatherMap API key to access weather data and location information. Get your API key from the [OpenWeatherMap API](https://openweathermap.org/api) | required | - |
| `override_coordinates` | Comma-separated coordinates (e.g., `37.8267,-122.4233`) to override device location | optional | - |
| `override_locale` | Override the default locale with a supported language code | optional | `en` |
Expand Down
41 changes: 41 additions & 0 deletions edge-apps/weather/e2e/screenshots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,26 @@ const { screenlyJsContent } = createMockScreenlyForScreenshots(
location: 'Mountain View, CA',
},
{
display_errors: 'false',
override_timezone: 'America/Los_Angeles',
override_locale: 'en',
openweathermap_api_key: 'mock-api-key',
},
)

const { screenlyJsContent: screenlyJsContentNoApiKey } =
createMockScreenlyForScreenshots(
{
coordinates: [37.3893889, -122.0832101],
location: 'Mountain View, CA',
},
{
display_errors: 'false',
override_timezone: 'America/Los_Angeles',
override_locale: 'en',
},
)

for (const { width, height } of RESOLUTIONS) {
test(`screenshot ${width}x${height}`, async ({ browser }) => {
const screenshotsDir = getScreenshotsDir()
Expand Down Expand Up @@ -53,3 +67,30 @@ for (const { width, height } of RESOLUTIONS) {
await context.close()
})
}

const NO_API_KEY_RESOLUTIONS = [
{ width: 3840, height: 2160 },
{ width: 2160, height: 3840 },
]

for (const { width, height } of NO_API_KEY_RESOLUTIONS) {
test(`screenshot no-api-key ${width}x${height}`, async ({ browser }) => {
const screenshotsDir = getScreenshotsDir()

const context = await browser.newContext({ viewport: { width, height } })
const page = await context.newPage()

await setupClockMock(page)
await setupScreenlyJsMock(page, screenlyJsContentNoApiKey)

await page.goto('/')
await page.waitForLoadState('networkidle')

await page.screenshot({
path: path.join(screenshotsDir, `no-api-key-${width}x${height}.png`),
fullPage: false,
})

await context.close()
})
}
16 changes: 16 additions & 0 deletions edge-apps/weather/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@
<div class="forecast-items" data-forecast-items></div>
</section>
</main>

<div id="error-screen" style="display: none">
<div class="error-card">
<div class="error-card-left">
<h2>Something went wrong</h2>
<p>
The weather data could not be loaded. Please check your
configuration or contact your administrator.
</p>
</div>
<div class="error-card-right">
<div class="error-detail-label">Error details</div>
<p id="error-message" class="error-detail-message"></p>
</div>
</div>
</div>
</div>
</auto-scaler>

Expand Down
13 changes: 12 additions & 1 deletion edge-apps/weather/screenly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@ description: Displays the current weather and time
icon: https://playground.srly.io/edge-apps/weather/static/img/icon.svg
author: Screenly, Inc.
categories:
- Utilities
- Utilities
Comment thread
nicomiguelino marked this conversation as resolved.
ready_signal: true
settings:
display_errors:
type: string
default_value: 'false'
title: Display Errors
optional: true
help_text:
properties:
advanced: true
help_text: For debugging purposes to display errors on the screen.
type: boolean
schema_version: 1
enable_analytics:
type: string
default_value: 'true'
Expand Down
13 changes: 12 additions & 1 deletion edge-apps/weather/screenly_qc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@ description: Displays the current weather and time
icon: https://playground.srly.io/edge-apps/weather/static/img/icon.svg
author: Screenly, Inc.
categories:
- Utilities
- Utilities
Comment thread
nicomiguelino marked this conversation as resolved.
ready_signal: true
settings:
display_errors:
type: string
default_value: 'false'
title: Display Errors
optional: true
help_text:
properties:
advanced: true
help_text: For debugging purposes to display errors on the screen.
type: boolean
schema_version: 1
enable_analytics:
type: string
default_value: 'true'
Expand Down
Binary file modified edge-apps/weather/screenshots/1080x1920.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified edge-apps/weather/screenshots/1280x720.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified edge-apps/weather/screenshots/1920x1080.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified edge-apps/weather/screenshots/2160x3840.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified edge-apps/weather/screenshots/2160x4096.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified edge-apps/weather/screenshots/3840x2160.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified edge-apps/weather/screenshots/4096x2160.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified edge-apps/weather/screenshots/480x800.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified edge-apps/weather/screenshots/720x1280.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified edge-apps/weather/screenshots/800x480.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
102 changes: 102 additions & 0 deletions edge-apps/weather/src/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,93 @@ auto-scaler {
color: #e9e9e9;
}

/* Error screen */

#error-screen {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem;
z-index: 10;
}

.error-card {
background: linear-gradient(
106deg,
rgba(255, 255, 255, 0.09) 3%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1.054px solid rgba(255, 255, 255, 0.1);
border-radius: 1rem;
display: flex;
flex-direction: row;
overflow: hidden;
max-width: 60rem;
width: 100%;
backdrop-filter: blur(31.63px);
box-shadow:
0 1px 0 0 rgba(255, 255, 255, 0.15) inset,
0 -1px 0 0 rgba(255, 255, 255, 0.05) inset;
}

.error-card-left {
background: rgba(255, 255, 255, 0.08);
border-right: 0.0625rem solid rgba(255, 255, 255, 0.1);
padding: 2.5rem 2rem;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 1rem;
flex: 0 0 20rem;
}

.error-card-left h2 {
font-size: 2rem;
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.03em;
color: rgba(255, 255, 255, 0.95);
margin: 0;
text-shadow: none;
}

.error-card-left p {
font-size: 1.25rem;
color: rgba(255, 255, 255, 0.85);
line-height: 1.6;
margin: 0;
text-shadow: none;
}

.error-card-right {
flex: 1;
padding: 2.5rem 2rem;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 0.75rem;
}

.error-detail-label {
font-size: 0.75rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
letter-spacing: 0.08em;
text-shadow: none;
}

.error-detail-message {
font-size: 1.25rem;
color: rgba(255, 255, 255, 0.85);
line-height: 1.6;
word-break: break-word;
margin: 0;
text-shadow: none;
}

/* Portrait mode */
@media (orientation: portrait) {
.current-weather {
Expand Down Expand Up @@ -236,4 +323,19 @@ auto-scaler {
.forecast-item-time-period {
display: none;
}

#error-screen {
padding: 1.5rem 2rem;
}

.error-card {
flex-direction: column;
max-width: 100%;
}

.error-card-left {
flex: none;
border-right: none;
border-bottom: 0.0625rem solid rgba(255, 255, 255, 0.1);
}
}
69 changes: 53 additions & 16 deletions edge-apps/weather/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,41 @@ import {
getSetting,
getCityInfo,
resolveMeasurementUnit,
setupErrorHandling,
type MeasurementUnit,
} from '@screenly/edge-apps'
import '@screenly/edge-apps/components'
import { getCurrentWeather, getHourlyForecast } from './weather'
import {
getCurrentWeather,
getHourlyForecast,
MISSING_API_KEY_ERROR,
} from './weather'
import type { ForecastItem } from './weather'
import { updateBackground } from './background'
import sunIcon from '../static/images/sun.svg'

type ErrorReporter = (error: unknown) => void

function showError(error: unknown): void {
const message = error instanceof Error ? error.message : String(error)
const contentEl = document.querySelector<HTMLElement>('main.content')
const errorScreen = document.getElementById('error-screen')
const errorMessage = document.getElementById('error-message')

if (contentEl) contentEl.style.display = 'none'
if (errorScreen) errorScreen.style.display = 'flex'
if (errorMessage) errorMessage.textContent = message
}

function createErrorReporter(displayErrors: boolean): ErrorReporter {
if (displayErrors) {
return (error) => {
throw error instanceof Error ? error : new Error(String(error))
}
}
Comment thread
nicomiguelino marked this conversation as resolved.
return showError
}

// DOM elements
let locationEl: Element | null
let temperatureEl: Element | null
Expand Down Expand Up @@ -146,19 +173,28 @@ async function updateWeatherDisplay(
}

document.addEventListener('DOMContentLoaded', async () => {
setupErrorHandling()

Comment thread
nicomiguelino marked this conversation as resolved.
locationEl = document.querySelector('[data-location]')
temperatureEl = document.querySelector('[data-temperature]')
weatherDescriptionEl = document.querySelector('[data-weather-description]')
tempHighEl = document.querySelector('[data-temp-high]')
tempLowEl = document.querySelector('[data-temp-low]')
forecastItemsEl = document.querySelector('[data-forecast-items]')
forecastCardEl = document.querySelector('[data-forecast-card]')
forecastHeaderIconEl = document.querySelector('[data-forecast-header-icon]')

if (forecastHeaderIconEl) {
forecastHeaderIconEl.src = sunIcon
}

const displayErrors = getSetting<string>('display_errors') === 'true'
const reportError = createErrorReporter(displayErrors)

try {
locationEl = document.querySelector('[data-location]')
temperatureEl = document.querySelector('[data-temperature]')
weatherDescriptionEl = document.querySelector('[data-weather-description]')
tempHighEl = document.querySelector('[data-temp-high]')
tempLowEl = document.querySelector('[data-temp-low]')
forecastItemsEl = document.querySelector('[data-forecast-items]')
forecastCardEl = document.querySelector('[data-forecast-card]')
forecastHeaderIconEl = document.querySelector('[data-forecast-header-icon]')

// Set forecast header icon
if (forecastHeaderIconEl) {
forecastHeaderIconEl.src = sunIcon
const apiKey = getSetting<string>('openweathermap_api_key')
if (!apiKey) {
throw new Error(MISSING_API_KEY_ERROR)
}

const [latitude, longitude] = getCoordinates()
Expand All @@ -184,8 +220,9 @@ document.addEventListener('DOMContentLoaded', async () => {
15 * 60 * 1000,
)
} catch (error) {
console.error('Failed to initialize app:', error)
console.error('Weather app initialization failed:', error)
reportError(error)
} finally {
signalReady()
}

signalReady()
})
28 changes: 16 additions & 12 deletions edge-apps/weather/src/weather.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import '@screenly/edge-apps/test'
import { describe, test, expect } from 'bun:test'
import { getCurrentWeather, getHourlyForecast } from './weather'
import {
getCurrentWeather,
getHourlyForecast,
MISSING_API_KEY_ERROR,
} from './weather'

let mockFetchCurrentWeatherData: (
lat: number,
Expand Down Expand Up @@ -204,19 +208,19 @@ describe('getHourlyForecast', () => {
displayTemp: '58°F',
}

test('should return empty array when no API key', async () => {
test('should throw when no API key is provided', async () => {
mockGetSetting = () => undefined

const result = await getHourlyForecast(
37.39,
-122.0812,
'America/Los_Angeles',
'en',
'imperial',
mockCurrentWeather,
)

expect(result).toEqual([])
await expect(
getHourlyForecast(
37.39,
-122.0812,
'America/Los_Angeles',
'en',
'imperial',
mockCurrentWeather,
),
).rejects.toThrow(MISSING_API_KEY_ERROR)
})

test('should prepend current weather as NOW and return forecast items', async () => {
Expand Down
Loading