use daisyui components

This commit is contained in:
Graham McIntire 2025-07-05 09:22:47 -05:00
parent 3789684d8c
commit 53dc32f8fc
No known key found for this signature in database
11 changed files with 1230 additions and 921 deletions

View file

@ -76,9 +76,41 @@ Hooks.APRSMap = MapAPRSMap;
Hooks.ResponsiveSlideoverHook = ResponsiveSlideoverHook;
Hooks.ChartJSTempChart = {
mounted() { this.renderChart(); },
mounted() {
console.log('Temperature chart mounted');
// Initialize global chart instances map if it doesn't exist
if (!window.chartInstances) {
window.chartInstances = new Map();
}
// Register this chart instance
window.chartInstances.set(this.el.id, this);
this.renderChart();
// Listen for theme changes
this.themeChangeHandler = () => {
console.log('Temperature chart received themeChanged event');
if (this.chart) {
console.log('Re-rendering temperature chart');
this.renderChart();
}
};
window.addEventListener('themeChanged', this.themeChangeHandler);
},
updated() { this.renderChart(); },
destroyed() {
// Clean up event listener
if (this.themeChangeHandler) {
window.removeEventListener('themeChanged', this.themeChangeHandler);
}
// Remove from global chart instances
if (window.chartInstances) {
window.chartInstances.delete(this.el.id);
}
},
renderChart() {
console.log('Temperature chart renderChart called');
if (this.chart) {
this.chart.destroy();
}
@ -87,6 +119,8 @@ Hooks.ChartJSTempChart = {
const times = data.map(d => new Date(d.timestamp));
const temps = data.map(d => d.temperature);
const dews = data.map(d => d.dew_point);
const colors = getThemeColors();
console.log('Temperature chart colors:', colors);
const canvas = this.el.querySelector('canvas');
if (!canvas) {
@ -121,7 +155,13 @@ Hooks.ChartJSTempChart = {
plugins: {
title: {
display: true,
text: 'Temperature & Dew Point (°F)'
text: 'Temperature & Dew Point (°F)',
color: colors.text
},
legend: {
labels: {
color: colors.text
}
}
},
scales: {
@ -135,13 +175,27 @@ Hooks.ChartJSTempChart = {
},
title: {
display: true,
text: 'Time'
text: 'Time',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
},
y: {
title: {
display: true,
text: '°F'
text: '°F',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
}
}
@ -151,16 +205,50 @@ Hooks.ChartJSTempChart = {
};
Hooks.ChartJSHumidityChart = {
mounted() { this.renderChart(); },
mounted() {
console.log('Humidity chart mounted');
// Initialize global chart instances map if it doesn't exist
if (!window.chartInstances) {
window.chartInstances = new Map();
}
// Register this chart instance
window.chartInstances.set(this.el.id, this);
this.renderChart();
// Listen for theme changes
this.themeChangeHandler = () => {
console.log('Humidity chart received themeChanged event');
if (this.chart) {
console.log('Re-rendering humidity chart');
this.renderChart();
}
};
window.addEventListener('themeChanged', this.themeChangeHandler);
},
updated() { this.renderChart(); },
destroyed() {
// Clean up event listener
if (this.themeChangeHandler) {
window.removeEventListener('themeChanged', this.themeChangeHandler);
}
// Remove from global chart instances
if (window.chartInstances) {
window.chartInstances.delete(this.el.id);
}
},
renderChart() {
console.log('Humidity chart renderChart called');
if (this.chart) {
this.chart.destroy();
}
const data = JSON.parse(this.el.dataset.weatherHistory);
const times = data.map(d => new Date(d.timestamp));
const hums = data.map(d => d.humidity);
const humidity = data.map(d => d.humidity);
const colors = getThemeColors();
console.log('Humidity chart colors:', colors);
const canvas = this.el.querySelector('canvas');
if (!canvas) {
@ -174,9 +262,9 @@ Hooks.ChartJSHumidityChart = {
labels: times,
datasets: [{
label: 'Humidity (%)',
data: hums,
borderColor: 'blue',
backgroundColor: 'rgba(0, 0, 255, 0.1)',
data: humidity,
borderColor: 'green',
backgroundColor: 'rgba(0, 255, 0, 0.1)',
tension: 0.1
}]
},
@ -186,7 +274,13 @@ Hooks.ChartJSHumidityChart = {
plugins: {
title: {
display: true,
text: 'Humidity (%)'
text: 'Humidity (%)',
color: colors.text
},
legend: {
labels: {
color: colors.text
}
}
},
scales: {
@ -200,13 +294,27 @@ Hooks.ChartJSHumidityChart = {
},
title: {
display: true,
text: 'Time'
text: 'Time',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
},
y: {
title: {
display: true,
text: '%'
text: '%',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
}
}
@ -216,16 +324,50 @@ Hooks.ChartJSHumidityChart = {
};
Hooks.ChartJSPressureChart = {
mounted() { this.renderChart(); },
mounted() {
console.log('Pressure chart mounted');
// Initialize global chart instances map if it doesn't exist
if (!window.chartInstances) {
window.chartInstances = new Map();
}
// Register this chart instance
window.chartInstances.set(this.el.id, this);
this.renderChart();
// Listen for theme changes
this.themeChangeHandler = () => {
console.log('Pressure chart received themeChanged event');
if (this.chart) {
console.log('Re-rendering pressure chart');
this.renderChart();
}
};
window.addEventListener('themeChanged', this.themeChangeHandler);
},
updated() { this.renderChart(); },
destroyed() {
// Clean up event listener
if (this.themeChangeHandler) {
window.removeEventListener('themeChanged', this.themeChangeHandler);
}
// Remove from global chart instances
if (window.chartInstances) {
window.chartInstances.delete(this.el.id);
}
},
renderChart() {
console.log('Pressure chart renderChart called');
if (this.chart) {
this.chart.destroy();
}
const data = JSON.parse(this.el.dataset.weatherHistory);
const times = data.map(d => new Date(d.timestamp));
const pressures = data.map(d => d.pressure);
const pressure = data.map(d => d.pressure);
const colors = getThemeColors();
console.log('Pressure chart colors:', colors);
const canvas = this.el.querySelector('canvas');
if (!canvas) {
@ -238,138 +380,8 @@ Hooks.ChartJSPressureChart = {
data: {
labels: times,
datasets: [{
label: 'Pressure (hPa)',
data: pressures,
borderColor: 'green',
backgroundColor: 'rgba(0, 128, 0, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Pressure (hPa)'
}
},
scales: {
x: {
type: 'time',
time: {
displayFormats: {
hour: 'HH:mm',
minute: 'HH:mm'
}
},
title: {
display: true,
text: 'Time'
}
},
y: {
title: {
display: true,
text: 'hPa'
}
}
}
}
});
}
};
Hooks.ChartJSWindDirectionChart = {
mounted() { this.renderChart(); },
updated() { this.renderChart(); },
renderChart() {
if (this.chart) {
this.chart.destroy();
}
const data = JSON.parse(this.el.dataset.weatherHistory);
const times = data.map(d => new Date(d.timestamp));
const dirs = data.map(d => d.wind_direction);
const canvas = this.el.querySelector('canvas');
if (!canvas) {
console.error('Canvas element not found for wind direction chart');
return;
}
this.chart = new Chart(canvas, {
type: 'line',
data: {
labels: times,
datasets: [{
label: 'Wind Direction (°)',
data: dirs,
borderColor: 'orange',
backgroundColor: 'rgba(255, 165, 0, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Wind Direction (°)'
}
},
scales: {
x: {
type: 'time',
time: {
displayFormats: {
hour: 'HH:mm',
minute: 'HH:mm'
}
},
title: {
display: true,
text: 'Time'
}
},
y: {
title: {
display: true,
text: '°'
}
}
}
}
});
}
};
Hooks.ChartJSWindSpeedChart = {
mounted() { this.renderChart(); },
updated() { this.renderChart(); },
renderChart() {
if (this.chart) {
this.chart.destroy();
}
const data = JSON.parse(this.el.dataset.weatherHistory);
const times = data.map(d => new Date(d.timestamp));
const speeds = data.map(d => d.wind_speed);
const canvas = this.el.querySelector('canvas');
if (!canvas) {
console.error('Canvas element not found for wind speed chart');
return;
}
this.chart = new Chart(canvas, {
type: 'line',
data: {
labels: times,
datasets: [{
label: 'Wind Speed (mph)',
data: speeds,
label: 'Pressure (mb)',
data: pressure,
borderColor: 'purple',
backgroundColor: 'rgba(128, 0, 128, 0.1)',
tension: 0.1
@ -381,7 +393,13 @@ Hooks.ChartJSWindSpeedChart = {
plugins: {
title: {
display: true,
text: 'Wind Speed (mph)'
text: 'Pressure (mb)',
color: colors.text
},
legend: {
labels: {
color: colors.text
}
}
},
scales: {
@ -395,13 +413,156 @@ Hooks.ChartJSWindSpeedChart = {
},
title: {
display: true,
text: 'Time'
text: 'Time',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
},
y: {
title: {
display: true,
text: 'mph'
text: 'mb',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
}
}
}
});
}
};
Hooks.ChartJSWindChart = {
mounted() {
console.log('Wind chart mounted');
// Initialize global chart instances map if it doesn't exist
if (!window.chartInstances) {
window.chartInstances = new Map();
}
// Register this chart instance
window.chartInstances.set(this.el.id, this);
this.renderChart();
// Listen for theme changes
this.themeChangeHandler = () => {
console.log('Wind chart received themeChanged event');
if (this.chart) {
console.log('Re-rendering wind chart');
this.renderChart();
}
};
window.addEventListener('themeChanged', this.themeChangeHandler);
},
updated() { this.renderChart(); },
destroyed() {
// Clean up event listener
if (this.themeChangeHandler) {
window.removeEventListener('themeChanged', this.themeChangeHandler);
}
// Remove from global chart instances
if (window.chartInstances) {
window.chartInstances.delete(this.el.id);
}
},
renderChart() {
console.log('Wind chart renderChart called');
if (this.chart) {
this.chart.destroy();
}
const data = JSON.parse(this.el.dataset.weatherHistory);
const times = data.map(d => new Date(d.timestamp));
const windSpeed = data.map(d => d.wind_speed);
const windGust = data.map(d => d.wind_gust);
const colors = getThemeColors();
console.log('Wind chart colors:', colors);
const canvas = this.el.querySelector('canvas');
if (!canvas) {
console.error('Canvas element not found for wind chart');
return;
}
this.chart = new Chart(canvas, {
type: 'line',
data: {
labels: times,
datasets: [
{
label: 'Wind Speed (mph)',
data: windSpeed,
borderColor: 'orange',
backgroundColor: 'rgba(255, 165, 0, 0.1)',
tension: 0.1
},
{
label: 'Wind Gust (mph)',
data: windGust,
borderColor: 'brown',
backgroundColor: 'rgba(165, 42, 42, 0.1)',
tension: 0.1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Wind Speed & Gust (mph)',
color: colors.text
},
legend: {
labels: {
color: colors.text
}
}
},
scales: {
x: {
type: 'time',
time: {
displayFormats: {
hour: 'HH:mm',
minute: 'HH:mm'
}
},
title: {
display: true,
text: 'Time',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
},
y: {
title: {
display: true,
text: 'mph',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
}
}
@ -411,9 +572,41 @@ Hooks.ChartJSWindSpeedChart = {
};
Hooks.ChartJSRainChart = {
mounted() { this.renderChart(); },
mounted() {
console.log('Rain chart mounted');
// Initialize global chart instances map if it doesn't exist
if (!window.chartInstances) {
window.chartInstances = new Map();
}
// Register this chart instance
window.chartInstances.set(this.el.id, this);
this.renderChart();
// Listen for theme changes
this.themeChangeHandler = () => {
console.log('Rain chart received themeChanged event');
if (this.chart) {
console.log('Re-rendering rain chart');
this.renderChart();
}
};
window.addEventListener('themeChanged', this.themeChangeHandler);
},
updated() { this.renderChart(); },
destroyed() {
// Clean up event listener
if (this.themeChangeHandler) {
window.removeEventListener('themeChanged', this.themeChangeHandler);
}
// Remove from global chart instances
if (window.chartInstances) {
window.chartInstances.delete(this.el.id);
}
},
renderChart() {
console.log('Rain chart renderChart called');
if (this.chart) {
this.chart.destroy();
}
@ -422,7 +615,9 @@ Hooks.ChartJSRainChart = {
const times = data.map(d => new Date(d.timestamp));
const rain1h = data.map(d => d.rain_1h);
const rain24h = data.map(d => d.rain_24h);
const rainMid = data.map(d => d.rain_since_midnight);
const rainSinceMidnight = data.map(d => d.rain_since_midnight);
const colors = getThemeColors();
console.log('Rain chart colors:', colors);
const canvas = this.el.querySelector('canvas');
if (!canvas) {
@ -445,15 +640,15 @@ Hooks.ChartJSRainChart = {
{
label: 'Rain (24h)',
data: rain24h,
borderColor: 'navy',
backgroundColor: 'rgba(0, 0, 128, 0.1)',
borderColor: 'cyan',
backgroundColor: 'rgba(0, 255, 255, 0.1)',
tension: 0.1
},
{
label: 'Rain (since midnight)',
data: rainMid,
borderColor: 'teal',
backgroundColor: 'rgba(0, 128, 128, 0.1)',
data: rainSinceMidnight,
borderColor: 'navy',
backgroundColor: 'rgba(0, 0, 128, 0.1)',
tension: 0.1
}
]
@ -464,7 +659,13 @@ Hooks.ChartJSRainChart = {
plugins: {
title: {
display: true,
text: 'Rain (inches)'
text: 'Rainfall (inches)',
color: colors.text
},
legend: {
labels: {
color: colors.text
}
}
},
scales: {
@ -478,13 +679,27 @@ Hooks.ChartJSRainChart = {
},
title: {
display: true,
text: 'Time'
text: 'Time',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
},
y: {
title: {
display: true,
text: 'in'
text: 'inches',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
}
}
@ -494,16 +709,50 @@ Hooks.ChartJSRainChart = {
};
Hooks.ChartJSLuminosityChart = {
mounted() { this.renderChart(); },
mounted() {
console.log('Luminosity chart mounted');
// Initialize global chart instances map if it doesn't exist
if (!window.chartInstances) {
window.chartInstances = new Map();
}
// Register this chart instance
window.chartInstances.set(this.el.id, this);
this.renderChart();
// Listen for theme changes
this.themeChangeHandler = () => {
console.log('Luminosity chart received themeChanged event');
if (this.chart) {
console.log('Re-rendering luminosity chart');
this.renderChart();
}
};
window.addEventListener('themeChanged', this.themeChangeHandler);
},
updated() { this.renderChart(); },
destroyed() {
// Clean up event listener
if (this.themeChangeHandler) {
window.removeEventListener('themeChanged', this.themeChangeHandler);
}
// Remove from global chart instances
if (window.chartInstances) {
window.chartInstances.delete(this.el.id);
}
},
renderChart() {
console.log('Luminosity chart renderChart called');
if (this.chart) {
this.chart.destroy();
}
const data = JSON.parse(this.el.dataset.weatherHistory);
const times = data.map(d => new Date(d.timestamp));
const lum = data.map(d => d.luminosity);
const luminosity = data.map(d => d.luminosity);
const colors = getThemeColors();
console.log('Luminosity chart colors:', colors);
const canvas = this.el.querySelector('canvas');
if (!canvas) {
@ -517,9 +766,9 @@ Hooks.ChartJSLuminosityChart = {
labels: times,
datasets: [{
label: 'Luminosity',
data: lum,
borderColor: 'gold',
backgroundColor: 'rgba(255, 215, 0, 0.1)',
data: luminosity,
borderColor: 'yellow',
backgroundColor: 'rgba(255, 255, 0, 0.1)',
tension: 0.1
}]
},
@ -529,7 +778,13 @@ Hooks.ChartJSLuminosityChart = {
plugins: {
title: {
display: true,
text: 'Luminosity'
text: 'Luminosity',
color: colors.text
},
legend: {
labels: {
color: colors.text
}
}
},
scales: {
@ -543,13 +798,27 @@ Hooks.ChartJSLuminosityChart = {
},
title: {
display: true,
text: 'Time'
text: 'Time',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
},
y: {
title: {
display: true,
text: ''
text: 'Luminosity',
color: colors.text
},
ticks: {
color: colors.text
},
grid: {
color: colors.grid
}
}
}
@ -558,6 +827,99 @@ Hooks.ChartJSLuminosityChart = {
}
};
// Helper function to get theme-aware colors
const getThemeColors = () => {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
return {
text: isDark ? '#e5e7eb' : '#111827',
grid: isDark ? '#374151' : '#9ca3af',
background: isDark ? 'rgba(0, 0, 0, 0.1)' : 'rgba(255, 255, 255, 0.1)'
};
};
// Theme switching functionality
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
return 'auto';
})();
const applyTheme = (theme) => {
const element = document.documentElement;
if (theme === 'auto') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
element.setAttribute('data-theme', 'dark');
} else {
element.setAttribute('data-theme', 'light');
}
} else {
element.setAttribute('data-theme', theme);
}
};
// Apply initial theme
applyTheme(theme);
window.localStorage.setItem('theme', theme);
// Global function to re-render all charts
window.reRenderAllCharts = () => {
console.log('Re-rendering all charts...');
// Store all chart instances globally so we can access them
if (!window.chartInstances) {
window.chartInstances = new Map();
}
// Re-render all stored chart instances
window.chartInstances.forEach((chartInstance, elementId) => {
console.log('Re-rendering chart:', elementId);
if (chartInstance && typeof chartInstance.renderChart === 'function') {
chartInstance.renderChart();
}
});
// Also dispatch a custom event that charts can listen to
console.log('Dispatching themeChanged event');
window.dispatchEvent(new CustomEvent('themeChanged'));
};
const handleThemeClick = (selectedTheme) => {
console.log('Theme changed to:', selectedTheme);
applyTheme(selectedTheme);
localStorage.setItem('theme', selectedTheme);
// Re-render all charts with new theme colors
setTimeout(() => {
console.log('Calling reRenderAllCharts after theme change');
window.reRenderAllCharts();
}, 100);
};
// Listen for system theme changes when auto is selected
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (localStorage.getItem('theme') === 'auto') {
applyTheme('auto');
// Re-render all charts with new theme colors
setTimeout(() => {
window.reRenderAllCharts();
}, 100);
}
});
// Add event listeners for theme switching
document.addEventListener('DOMContentLoaded', () => {
const themeButtons = document.querySelectorAll('[data-set-theme]');
themeButtons.forEach(button => {
button.addEventListener('click', () => {
const theme = button.getAttribute('data-set-theme');
handleThemeClick(theme);
});
});
});
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken },

View file

@ -444,19 +444,86 @@ defmodule AprsmeWeb.CoreComponents do
def header(assigns) do
~H"""
<header class={["bg-white shadow sticky top-0 z-40", @class]}>
<nav
class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 flex h-16 items-center justify-between"
aria-label="Main"
>
<div class="flex items-center space-x-4">
<a href="/" class="text-xl font-bold text-blue-700 hover:text-blue-900 transition-colors">
aprs.me
</a>
</div>
<div class="navbar bg-base-200 shadow-lg">
<div class="navbar-start">
<a href="/" class="btn btn-ghost text-xl font-bold text-primary">
aprs.me
</a>
</div>
<div class="navbar-center hidden lg:flex">
<.navigation variant={:horizontal} />
</nav>
</header>
</div>
<div class="navbar-end">
<.theme_selector />
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</div>
<ul
tabindex="0"
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
>
<.navigation variant={:vertical} />
</ul>
</div>
</div>
</div>
"""
end
@doc """
Renders a theme selector dropdown.
"""
def theme_selector(assigns) do
~H"""
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
<svg class="w-5 h-5 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z" />
</svg>
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<li>
<button data-set-theme="light" data-act-class="ACTIVECLASS" class="flex items-center gap-2">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clip-rule="evenodd"
/>
</svg>
Light
</button>
</li>
<li>
<button data-set-theme="dark" data-act-class="ACTIVECLASS" class="flex items-center gap-2">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
Dark
</button>
</li>
<li>
<button data-set-theme="auto" data-act-class="ACTIVECLASS" class="flex items-center gap-2">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clip-rule="evenodd"
/>
</svg>
Auto
</button>
</li>
</ul>
</div>
"""
end
@ -682,19 +749,17 @@ defmodule AprsmeWeb.CoreComponents do
def navigation(assigns) do
~H"""
<nav class={[
"flex items-center space-x-6",
@variant == :vertical && "flex-col space-x-0 space-y-3 items-start",
@class
]}>
<a href="/" class="text-gray-700 hover:text-blue-700 font-medium transition-colors">Home</a>
<a href="/api" class="text-gray-700 hover:text-blue-700 font-medium transition-colors">
API
</a>
<a href="/about" class="text-gray-700 hover:text-blue-700 font-medium transition-colors">
About
</a>
</nav>
<%= if @variant == :horizontal do %>
<ul class="menu menu-horizontal px-1">
<li><a href="/" class="link link-hover">Home</a></li>
<li><a href="/api" class="link link-hover">API</a></li>
<li><a href="/about" class="link link-hover">About</a></li>
</ul>
<% else %>
<li><a href="/" class="link link-hover">Home</a></li>
<li><a href="/api" class="link link-hover">API</a></li>
<li><a href="/about" class="link link-hover">About</a></li>
<% end %>
"""
end
end

View file

@ -5,6 +5,6 @@ defmodule AprsmeWeb.Layouts do
embed_templates "layouts/*"
def body_class(_assigns) do
["bg-white antialiased"]
["bg-base-100 antialiased"]
end
end

View file

@ -1,5 +1,5 @@
<.header />
<main class="min-h-screen bg-white">
<main class="min-h-screen bg-base-100">
<div>
<.flash kind={:info} title="Success!" flash={@flash} />
<.flash kind={:error} title="Error!" flash={@flash} />

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

View file

@ -1,69 +1,45 @@
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<div class="max-w-4xl mx-auto px-4 py-12">
<!-- Main Content -->
<div class="bg-white rounded-2xl shadow-xl p-8 mb-8">
<div class="prose prose-lg max-w-none">
<h2 class="text-3xl font-bold text-gray-900 mb-6">Under heavy construction</h2>
<p class="text-gray-700 mb-6 leading-relaxed">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
</p>
<p class="text-gray-700 mb-8 leading-relaxed">
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
</p>
<div class="bg-gray-50 p-6 rounded-lg mb-8">
<h4 class="text-lg font-semibold text-gray-900 mb-3">Built with Modern Technologies</h4>
<div class="flex flex-wrap gap-3">
<span class="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
Elixir
</span>
<span class="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm font-medium">
Phoenix
</span>
<span class="px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm font-medium">
LiveView
</span>
<span class="px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-sm font-medium">
PostgreSQL
</span>
<span class="px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium">
Tailwind CSS
</span>
</div>
</div>
<h3 class="text-2xl font-semibold text-gray-900 mb-4">Join Our Community</h3>
<p class="text-gray-700 mb-6 leading-relaxed">
Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.
</p>
<div class="text-center mt-12">
<a
href="/"
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m-6 3l6-3"
>
</path>
</svg>
Explore the Map
</a>
<div class="container mx-auto max-w-2xl py-12">
<div class="card bg-base-100 shadow-xl mb-8">
<div class="card-body prose max-w-none">
<h2 class="card-title text-3xl">Under heavy construction</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
</p>
<p>
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
</p>
<div class="bg-base-200 p-6 rounded-lg mb-8">
<h4 class="text-lg font-semibold mb-3">Built with Modern Technologies</h4>
<div class="flex flex-wrap gap-2">
<span class="badge badge-primary">Elixir</span>
<span class="badge badge-success">Phoenix</span>
<span class="badge badge-secondary">LiveView</span>
<span class="badge badge-warning">PostgreSQL</span>
<span class="badge badge-error">Tailwind CSS</span>
</div>
</div>
</div>
<!-- Footer Section -->
<div class="text-center text-gray-500">
<p class="text-sm">
© 2025 aprs.me. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<h3 class="text-2xl font-semibold mb-4">Join Our Community</h3>
<p>
Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.
</p>
<div class="text-center mt-12">
<a href="/" class="btn btn-primary btn-lg">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m-6 3l6-3"
/>
</svg>
Explore the Map
</a>
</div>
</div>
</div>
<div class="text-center text-base-content/60">
<p class="text-sm">
&copy; 2025 aprs.me. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>
</div>
</div>

View file

@ -213,48 +213,58 @@ defmodule AprsmeWeb.ApiDocsLive do
@impl true
def render(assigns) do
~H"""
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="container mx-auto max-w-6xl py-8">
<div class="mb-8">
<h1 class="text-4xl font-bold text-gray-900 mb-4">APRS.me API Documentation</h1>
<p class="text-lg text-gray-600">
<h1 class="text-4xl font-bold mb-4">APRS.me API Documentation</h1>
<p class="text-lg opacity-70">
RESTful JSON API for accessing APRS packet data and station information.
</p>
</div>
<!-- API Overview -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 mb-8">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-2xl font-semibold text-gray-900">Overview</h2>
</div>
<div class="px-6 py-4">
<div class="prose max-w-none">
<p class="text-gray-700 mb-4">
The APRS.me API provides programmatic access to Amateur Radio APRS (Automatic Packet Reporting System)
data. All API endpoints return JSON data and follow RESTful conventions.
</p>
<div class="card bg-base-100 shadow-xl mb-8">
<div class="card-body">
<h2 class="card-title text-2xl">Overview</h2>
<p class="mb-4">
The APRS.me API provides programmatic access to Amateur Radio APRS (Automatic Packet Reporting System)
data. All API endpoints return JSON data and follow RESTful conventions.
</p>
<div class="grid md:grid-cols-2 gap-6 mt-6">
<div class="bg-blue-50 p-4 rounded-lg">
<h3 class="font-semibold text-blue-900 mb-2">Base URL</h3>
<code class="text-sm bg-white px-2 py-1 rounded border">
https://aprs.me/api/v1
</code>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<h3 class="font-semibold text-green-900 mb-2">Content Type</h3>
<code class="text-sm bg-white px-2 py-1 rounded border">
application/json
</code>
</div>
<div class="grid md:grid-cols-2 gap-6 mt-6">
<div class="bg-primary/10 p-4 rounded-lg">
<h3 class="font-semibold mb-2">Base URL</h3>
<code class="text-sm bg-base-200 px-2 py-1 rounded">
https://aprs.me/api/v1
</code>
</div>
<div class="mt-6 p-4 bg-yellow-50 rounded-lg">
<h3 class="font-semibold text-yellow-900 mb-2">Rate Limiting</h3>
<p class="text-yellow-800">
Currently no rate limiting is enforced, but please be respectful and avoid excessive requests.
Rate limiting may be implemented in the future.
</p>
<div class="bg-success/10 p-4 rounded-lg">
<h3 class="font-semibold mb-2">Content Type</h3>
<code class="text-sm bg-base-200 px-2 py-1 rounded">
application/json
</code>
</div>
</div>
<div class="alert alert-warning mt-6">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<div>
<h3 class="font-bold">Rate Limiting</h3>
<div class="text-xs">
Currently no rate limiting is enforced, but please be respectful and avoid excessive requests. Rate limiting may be implemented in the future.
</div>
</div>
</div>
</div>
@ -263,66 +273,46 @@ defmodule AprsmeWeb.ApiDocsLive do
<!-- API Endpoints -->
<div class="space-y-8">
<!-- Callsign Endpoint -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-900">Get Latest Packet by Callsign</h2>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
GET
</span>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<h2 class="card-title text-xl">Get Latest Packet by Callsign</h2>
<span class="badge badge-success">GET</span>
</div>
</div>
<div class="px-6 py-4">
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">Endpoint</h3>
<div class="bg-gray-50 p-3 rounded-lg">
<h3 class="text-lg font-medium mb-2">Endpoint</h3>
<div class="bg-base-200 p-3 rounded-lg">
<code class="text-sm">GET /api/v1/callsign/{"{callsign}"}</code>
</div>
</div>
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">Description</h3>
<p class="text-gray-700">
<h3 class="text-lg font-medium mb-2">Description</h3>
<p>
Retrieves the most recent APRS packet for the specified callsign. The callsign can include
an SSID (e.g., N0CALL-9) or just the base callsign (e.g., N0CALL).
</p>
</div>
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">Parameters</h3>
<h3 class="text-lg font-medium mb-2">Parameters</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<table class="table table-zebra">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Parameter
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Required
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
<th>Parameter</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tbody>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
callsign
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
string
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
Yes
</td>
<td class="px-6 py-4 text-sm text-gray-900">
Amateur radio callsign with optional SSID (e.g., N0CALL or N0CALL-9)
</td>
<td class="font-mono">callsign</td>
<td>string</td>
<td>Yes</td>
<td>Amateur radio callsign with optional SSID (e.g., N0CALL or N0CALL-9)</td>
</tr>
</tbody>
</table>
@ -330,18 +320,18 @@ defmodule AprsmeWeb.ApiDocsLive do
</div>
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">Example Request</h3>
<div class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto">
<h3 class="text-lg font-medium mb-2">Example Request</h3>
<div class="bg-neutral text-neutral-content p-4 rounded-lg overflow-x-auto">
<pre><code>curl -X GET "https://aprs.me/api/v1/callsign/N0CALL-9" \
-H "Accept: application/json"</code></pre>
</div>
</div>
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">Response Format</h3>
<h3 class="text-lg font-medium mb-2">Response Format</h3>
<h4 class="text-md font-medium text-gray-800 mb-2">Success Response (200 OK)</h4>
<div class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto mb-4">
<h4 class="text-md font-medium mb-2">Success Response (200 OK)</h4>
<div class="bg-neutral text-neutral-content p-4 rounded-lg overflow-x-auto mb-4">
<pre><code><%= raw ~s|{
"data": {
"id": 12345,
@ -383,16 +373,16 @@ defmodule AprsmeWeb.ApiDocsLive do
}| %></code></pre>
</div>
<h4 class="text-md font-medium text-gray-800 mb-2">Not Found Response (404)</h4>
<div class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto mb-4">
<h4 class="text-md font-medium mb-2">Not Found Response (404)</h4>
<div class="bg-neutral text-neutral-content p-4 rounded-lg overflow-x-auto mb-4">
<pre><code><%= raw ~s|{
"data": null,
"message": "No packets found for callsign N0CALL-9"
}| %></code></pre>
</div>
<h4 class="text-md font-medium text-gray-800 mb-2">Error Response (400)</h4>
<div class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto">
<h4 class="text-md font-medium mb-2">Error Response (400)</h4>
<div class="bg-neutral text-neutral-content p-4 rounded-lg overflow-x-auto">
<pre><code><%= raw ~s|{
"error": {
"message": "Invalid callsign format",
@ -405,118 +395,73 @@ defmodule AprsmeWeb.ApiDocsLive do
</div>
<!-- Response Fields Documentation -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900">Response Fields</h2>
</div>
<div class="px-6 py-4">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl">Response Fields</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<table class="table table-zebra">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Field
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tbody>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">id</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">integer</td>
<td class="px-6 py-4 text-sm text-gray-900">Unique packet identifier</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
callsign
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">string</td>
<td class="px-6 py-4 text-sm text-gray-900">
Full callsign with SSID (e.g., "N0CALL-9")
</td>
<td class="font-mono">id</td>
<td>integer</td>
<td>Unique packet identifier</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
base_callsign
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">string</td>
<td class="px-6 py-4 text-sm text-gray-900">Base callsign without SSID</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">ssid</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">string</td>
<td class="px-6 py-4 text-sm text-gray-900">
SSID (Secondary Station Identifier), null if not present
</td>
<td class="font-mono">callsign</td>
<td>string</td>
<td>Full callsign with SSID (e.g., "N0CALL-9")</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
received_at
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">datetime</td>
<td class="px-6 py-4 text-sm text-gray-900">
When the packet was received (ISO 8601 format)
</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
position
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">object</td>
<td class="px-6 py-4 text-sm text-gray-900">
Position data (latitude, longitude, course, speed, altitude)
</td>
<td class="font-mono">base_callsign</td>
<td>string</td>
<td>Base callsign without SSID</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
symbol
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">object</td>
<td class="px-6 py-4 text-sm text-gray-900">
APRS symbol information (code and table_id)
</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
weather
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">object</td>
<td class="px-6 py-4 text-sm text-gray-900">
Weather data if present (temperature, humidity, wind, etc.)
</td>
<td class="font-mono">ssid</td>
<td>string</td>
<td>SSID (Secondary Station Identifier), null if not present</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
equipment
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">object</td>
<td class="px-6 py-4 text-sm text-gray-900">
Equipment information (manufacturer, type)
</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
message
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">object</td>
<td class="px-6 py-4 text-sm text-gray-900">
Message data if the packet is a message
</td>
<td class="font-mono">received_at</td>
<td>datetime</td>
<td>When the packet was received (ISO 8601 format)</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
raw_packet
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">string</td>
<td class="px-6 py-4 text-sm text-gray-900">
Original raw APRS packet as received
</td>
<td class="font-mono">position</td>
<td>object</td>
<td>Position data (latitude, longitude, course, speed, altitude)</td>
</tr>
<tr>
<td class="font-mono">symbol</td>
<td>object</td>
<td>APRS symbol information (code and table_id)</td>
</tr>
<tr>
<td class="font-mono">weather</td>
<td>object</td>
<td>Weather data if present (temperature, humidity, wind, etc.)</td>
</tr>
<tr>
<td class="font-mono">equipment</td>
<td>object</td>
<td>Equipment information (manufacturer, type)</td>
</tr>
<tr>
<td class="font-mono">message</td>
<td>object</td>
<td>Message data if the packet is a message</td>
</tr>
<tr>
<td class="font-mono">raw_packet</td>
<td>string</td>
<td>Original raw APRS packet as received</td>
</tr>
</tbody>
</table>
@ -525,62 +470,37 @@ defmodule AprsmeWeb.ApiDocsLive do
</div>
<!-- HTTP Status Codes -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900">HTTP Status Codes</h2>
</div>
<div class="px-6 py-4">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl">HTTP Status Codes</h2>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<table class="table table-zebra">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status Code
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
<th>Status Code</th>
<th>Description</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tbody>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-green-600">
200 OK
</td>
<td class="px-6 py-4 text-sm text-gray-900">
Request successful, packet data returned
</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-yellow-600">
400 Bad Request
</td>
<td class="px-6 py-4 text-sm text-gray-900">
Invalid callsign format or malformed request
</td>
<td class="font-mono text-success">200 OK</td>
<td>Request successful, packet data returned</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-red-600">
404 Not Found
</td>
<td class="px-6 py-4 text-sm text-gray-900">
No packets found for the specified callsign
</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-red-600">
408 Request Timeout
</td>
<td class="px-6 py-4 text-sm text-gray-900">Request took too long to process</td>
<td class="font-mono text-warning">400 Bad Request</td>
<td>Invalid callsign format or malformed request</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-red-600">
500 Internal Server Error
</td>
<td class="px-6 py-4 text-sm text-gray-900">
Server error occurred while processing the request
</td>
<td class="font-mono text-error">404 Not Found</td>
<td>No packets found for the specified callsign</td>
</tr>
<tr>
<td class="font-mono text-error">408 Request Timeout</td>
<td>Request took too long to process</td>
</tr>
<tr>
<td class="font-mono text-error">500 Internal Server Error</td>
<td>Server error occurred while processing the request</td>
</tr>
</tbody>
</table>
@ -589,68 +509,62 @@ defmodule AprsmeWeb.ApiDocsLive do
</div>
<!-- Future Endpoints -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900">Planned Endpoints</h2>
</div>
<div class="px-6 py-4">
<p class="text-gray-700 mb-4">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl">Planned Endpoints</h2>
<p class="mb-4">
The following endpoints are planned for future releases:
</p>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div class="flex items-center justify-between p-3 bg-primary/10 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/callsign/{"{callsign}"}/history</code>
<p class="text-sm text-gray-600 mt-1">Get historical packets for a callsign</p>
<p class="text-sm opacity-70 mt-1">Get historical packets for a callsign</p>
</div>
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">Planned</span>
<span class="badge badge-primary">Planned</span>
</div>
<div class="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div class="flex items-center justify-between p-3 bg-primary/10 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/packets/recent</code>
<p class="text-sm text-gray-600 mt-1">Get recent packets with filtering options</p>
<p class="text-sm opacity-70 mt-1">Get recent packets with filtering options</p>
</div>
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">Planned</span>
<span class="badge badge-primary">Planned</span>
</div>
<div class="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div class="flex items-center justify-between p-3 bg-primary/10 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/packets/area</code>
<p class="text-sm text-gray-600 mt-1">Get packets within a geographic area</p>
<p class="text-sm opacity-70 mt-1">Get packets within a geographic area</p>
</div>
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">Planned</span>
<span class="badge badge-primary">Planned</span>
</div>
<div class="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div class="flex items-center justify-between p-3 bg-primary/10 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/weather/{"{callsign}"}</code>
<p class="text-sm text-gray-600 mt-1">Get weather data from weather stations</p>
<p class="text-sm opacity-70 mt-1">Get weather data from weather stations</p>
</div>
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">Planned</span>
<span class="badge badge-primary">Planned</span>
</div>
</div>
</div>
</div>
<!-- Interactive API Testing -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900">Test the API</h2>
</div>
<div class="px-6 py-4">
<p class="text-gray-700 mb-4">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl">Test the API</h2>
<p class="mb-4">
Try the API directly from this page. Enter a callsign to see the most recent packet data.
</p>
<div class="max-w-md">
<form phx-submit="test_api" class="space-y-4">
<div>
<label for="test_callsign" class="block text-sm font-medium text-gray-700 mb-2">
Callsign (e.g., N0CALL or N0CALL-9)
<label for="test_callsign" class="label">
<span class="label-text">Callsign (e.g., N0CALL or N0CALL-9)</span>
</label>
<div class="flex space-x-2">
<input
@ -660,38 +574,16 @@ defmodule AprsmeWeb.ApiDocsLive do
value={@test_callsign}
phx-change="update_callsign"
placeholder="Enter callsign..."
class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
class="input input-bordered flex-1"
disabled={@loading}
/>
<button
type="submit"
disabled={@loading or String.trim(@test_callsign) == ""}
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
class="btn btn-primary"
>
<%= if @loading do %>
<svg
class="animate-spin -ml-1 mr-2 h-4 w-4 text-white inline"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
>
</circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
>
</path>
</svg>
Testing...
<span class="loading loading-spinner loading-sm"></span> Testing...
<% else %>
Test API
<% end %>
@ -702,42 +594,42 @@ defmodule AprsmeWeb.ApiDocsLive do
<!-- Error Display -->
<%= if @error do %>
<div class="mt-4 p-3 bg-red-50 border border-red-200 rounded-md">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-red-800">{@error}</p>
</div>
</div>
<div class="alert alert-error mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{@error}</span>
</div>
<% end %>
<!-- Results Display -->
<%= if @api_result do %>
<div class="mt-4">
<h4 class="text-lg font-medium text-gray-900 mb-2">API Response</h4>
<h4 class="text-lg font-medium mb-2">API Response</h4>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<div class="bg-base-200 border rounded-lg p-4">
<div class="flex items-center mb-3">
<svg class="h-5 w-5 text-blue-500 mr-2" viewBox="0 0 20 20" fill="currentColor">
<svg class="h-5 w-5 text-primary mr-2" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
clip-rule="evenodd"
/>
</svg>
<span class="text-gray-800 font-medium">JSON Response</span>
<span class="font-medium">JSON Response</span>
</div>
<div class="bg-gray-900 text-white p-4 rounded text-sm overflow-x-auto">
<div class="bg-neutral text-neutral-content p-4 rounded text-sm overflow-x-auto">
<pre><%= @api_result %></pre>
</div>
</div>
@ -748,27 +640,22 @@ defmodule AprsmeWeb.ApiDocsLive do
</div>
<!-- Contact and Support -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900">Support</h2>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl">Support</h2>
<p class="mb-4">
This API is provided free of charge for amateur radio and educational purposes.
If you encounter issues or have suggestions for improvements, please reach out.
</p>
<div class="px-6 py-4">
<div class="prose max-w-none">
<p class="text-gray-700 mb-4">
This API is provided free of charge for amateur radio and educational purposes.
If you encounter issues or have suggestions for improvements, please reach out.
</p>
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="font-semibold text-gray-900 mb-2">Guidelines</h3>
<ul class="text-gray-700 space-y-1">
<li> Use reasonable request rates to avoid overwhelming the service</li>
<li> Cache responses when appropriate to reduce server load</li>
<li> Include a User-Agent header identifying your application</li>
<li> This service is for amateur radio and educational use</li>
</ul>
</div>
<div class="bg-base-200 p-4 rounded-lg">
<h3 class="font-semibold mb-2">Guidelines</h3>
<ul class="space-y-1">
<li> Use reasonable request rates to avoid overwhelming the service</li>
<li> Cache responses when appropriate to reduce server load</li>
<li> Include a User-Agent header identifying your application</li>
<li> This service is for amateur radio and educational use</li>
</ul>
</div>
</div>
</div>

View file

@ -16,18 +16,18 @@
<% _symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %>
<% _symbol_index = symbol_code_ord - 33 %>
<div class="min-h-screen bg-gray-50">
<div class="min-h-screen bg-base-200">
<!-- Page header -->
<div class="bg-white shadow-sm border-b">
<div class="px-4 sm:px-6 lg:px-8">
<div class="bg-base-100 shadow-sm">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-4 md:flex md:items-center md:justify-between">
<div class="min-w-0 flex-1">
<div class="flex items-center">
<h1 class="text-xl font-bold leading-7 text-gray-900 sm:truncate sm:text-2xl">
<h1 class="text-xl font-bold leading-7 sm:truncate sm:text-2xl">
APRS Station
</h1>
<div class="ml-3 flex items-center space-x-3">
<span class="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-sm font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
<span class="badge badge-primary">
{@callsign}
</span>
<%= if @packet do %>
@ -63,17 +63,11 @@
</div>
<div class="mt-3 flex md:ml-4 md:mt-0">
<div class="flex space-x-2">
<.link
navigate={~p"/packets/#{@callsign}"}
class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
<.link navigate={~p"/packets/#{@callsign}"} class="btn btn-outline btn-sm">
View packets
</.link>
<%= if AprsmeWeb.MapLive.PacketUtils.has_weather_packets?(@callsign) do %>
<.link
navigate={~p"/weather/#{@callsign}"}
class="inline-flex items-center rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
<.link navigate={~p"/weather/#{@callsign}"} class="btn btn-primary btn-sm">
Weather charts
</.link>
<% end %>
@ -83,17 +77,17 @@
</div>
</div>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
<div class="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
<%= if @packet do %>
<!-- Station details -->
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mb-6">
<!-- Position information -->
<div class="bg-white overflow-hidden shadow-sm rounded-lg border">
<div class="px-4 py-3">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 text-gray-400"
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
@ -112,38 +106,38 @@
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium text-gray-900">Position Information</h3>
<h3 class="text-sm font-medium">Position Information</h3>
</div>
</div>
<dl class="grid grid-cols-2 gap-3">
<div>
<dt class="text-xs font-medium text-gray-500">Location</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Location</dt>
<dd class="mt-1 text-sm font-semibold">
{@packet.lat || ""}, {@packet.lon || ""}
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Last Position</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Last Position</dt>
<dd class="mt-1 text-sm font-semibold">
{AprsmeWeb.MapLive.PacketUtils.get_timestamp(@packet)}
</dd>
</div>
<%= if @packet.altitude do %>
<div>
<dt class="text-xs font-medium text-gray-500">Altitude</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">{@packet.altitude} ft</dd>
<dt class="text-xs font-medium opacity-70">Altitude</dt>
<dd class="mt-1 text-sm font-semibold">{@packet.altitude} ft</dd>
</div>
<% end %>
<%= if @packet.course do %>
<div>
<dt class="text-xs font-medium text-gray-500">Course</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">{@packet.course}°</dd>
<dt class="text-xs font-medium opacity-70">Course</dt>
<dd class="mt-1 text-sm font-semibold">{@packet.course}°</dd>
</div>
<% end %>
<%= if @packet.speed do %>
<div>
<dt class="text-xs font-medium text-gray-500">Speed</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">{@packet.speed} MPH</dd>
<dt class="text-xs font-medium opacity-70">Speed</dt>
<dd class="mt-1 text-sm font-semibold">{@packet.speed} MPH</dd>
</div>
<% end %>
</dl>
@ -151,12 +145,12 @@
</div>
<!-- Device information -->
<div class="bg-white overflow-hidden shadow-sm rounded-lg border">
<div class="px-4 py-3">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 text-gray-400"
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
@ -170,13 +164,13 @@
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium text-gray-900">Device Information</h3>
<h3 class="text-sm font-medium">Device Information</h3>
</div>
</div>
<dl class="grid grid-cols-2 gap-3">
<div>
<dt class="text-xs font-medium text-gray-500">Device</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Device</dt>
<dd class="mt-1 text-sm font-semibold">
<%= if @packet.device_model || @packet.device_vendor do %>
{[
@packet.device_model,
@ -189,19 +183,19 @@
({@packet.device_class})
<% end %>
<% else %>
<span class="text-gray-500 font-mono">
<span class="opacity-70 font-mono">
{@packet.device_identifier || "Unknown"}
</span>
<% end %>
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Path</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">{@packet.path || ""}</dd>
<dt class="text-xs font-medium opacity-70">Path</dt>
<dd class="mt-1 text-sm font-semibold">{@packet.path || ""}</dd>
</div>
<div class="col-span-2">
<dt class="text-xs font-medium text-gray-500">Raw Packet</dt>
<dd class="mt-1 text-xs font-mono text-gray-900 break-all">
<dt class="text-xs font-medium opacity-70">Raw Packet</dt>
<dd class="mt-1 text-xs font-mono break-all">
<%= if is_binary(@packet.raw_packet) do %>
{Aprsme.EncodingUtils.sanitize_string(@packet.raw_packet)}
<% else %>
@ -215,12 +209,12 @@
</div>
<!-- Neighboring stations -->
<div class="bg-white shadow-sm rounded-lg border">
<div class="px-4 py-3">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 text-gray-400"
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
@ -234,42 +228,30 @@
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium text-gray-900">Stations Near Current Position</h3>
<h3 class="text-sm font-medium">Stations Near Current Position</h3>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<table class="table table-zebra">
<thead>
<tr>
<th
scope="col"
class="py-2 pl-3 pr-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
<th class="text-xs font-medium uppercase tracking-wider">
Callsign
</th>
<th
scope="col"
class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
<th class="text-xs font-medium uppercase tracking-wider">
Distance & Course
</th>
<th
scope="col"
class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
<th class="text-xs font-medium uppercase tracking-wider">
Last Heard
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tbody>
<%= for neighbor <- @neighbors do %>
<tr>
<td class="whitespace-nowrap py-2 pl-3 pr-2 text-sm font-medium text-gray-900">
<td class="font-medium">
<div class="flex items-center space-x-2">
<.link
navigate={~p"/info/#{neighbor.callsign}"}
class="text-blue-600 hover:text-blue-900"
>
<.link navigate={~p"/info/#{neighbor.callsign}"} class="link link-primary">
{neighbor.callsign}
</.link>
<%= if neighbor.packet do %>
@ -302,14 +284,14 @@
<% end %>
</div>
</td>
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500">
<td class="opacity-70">
<%= if neighbor.course do %>
{neighbor.distance || ""} @ {Float.round(neighbor.course, 0)}°
<% else %>
{neighbor.distance || ""}
<% end %>
</td>
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500">
<td class="opacity-70">
{neighbor.last_heard || ""}
</td>
</tr>
@ -322,7 +304,7 @@
<% else %>
<div class="text-center py-8">
<svg
class="mx-auto h-8 w-8 text-gray-400"
class="mx-auto h-8 w-8 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
@ -335,8 +317,8 @@
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423L16.5 15.75l.394 1.183a2.25 2.25 0 001.423 1.423L19.5 18.75l-1.183.394a2.25 2.25 0 00-1.423 1.423z"
/>
</svg>
<h3 class="mt-2 text-sm font-semibold text-gray-900">No data available</h3>
<p class="mt-1 text-sm text-gray-500">
<h3 class="mt-2 text-sm font-semibold">No data available</h3>
<p class="mt-1 text-sm opacity-70">
No recent packet data available for this callsign.
</p>
</div>

View file

@ -1,86 +1,106 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
<%= if @error do %>
<div class="mt-6 bg-red-50 border border-red-200 rounded-md p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Error</h3>
<div class="mt-1 text-sm text-red-700">
{@error}
</div>
<div class="alert alert-error">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
<div>
<h3 class="font-medium">Error</h3>
<div class="text-sm">
{@error}
</div>
</div>
</div>
<% else %>
<div class="mt-6 bg-white shadow-sm rounded-lg overflow-hidden">
<.table id="callsign-packets" rows={@all_packets}>
<:col :let={packet} label="Time">
<span class="text-sm text-gray-600 font-mono">
<%= case packet.received_at do %>
<% %DateTime{} = dt -> %>
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S")}
<% dt when is_binary(dt) -> %>
<%= case DateTime.from_iso8601(dt) do %>
<% {:ok, parsed_dt, _} -> %>
{Calendar.strftime(parsed_dt, "%Y-%m-%d %H:%M:%S")}
<% _ -> %>
{dt}
<% end %>
<% _ -> %>
N/A
<% end %>
</span>
</:col>
<:col :let={packet} label="Sender">
<span class="text-sm text-gray-900 font-medium">{packet.sender}</span>
</:col>
<:col :let={packet} label="Data Type">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{packet.data_type}
</span>
</:col>
<:col :let={packet} label="Information">
<span class="text-sm text-gray-900 font-mono">
<%= if String.length(packet.information_field || "") > 50 do %>
<span title={packet.information_field}>
{String.slice(packet.information_field, 0, 50)}...
</span>
<% else %>
{packet.information_field}
<% end %>
</span>
</:col>
<:col :let={packet} label="Path">
<span class="text-xs text-gray-500 font-mono">
{packet.path}
</span>
</:col>
<:col :let={packet} label="Device">
<span class="text-xs text-gray-700 font-mono">
<%= if packet.device_model || packet.device_vendor do %>
{[packet.device_model, packet.device_vendor]
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")}
<% else %>
{Map.get(packet, :device_identifier, Map.get(packet, "device_identifier", ""))}
<% end %>
</span>
</:col>
</.table>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Time</th>
<th>Sender</th>
<th>Data Type</th>
<th>Information</th>
<th>Path</th>
<th>Device</th>
</tr>
</thead>
<tbody>
<%= for packet <- @all_packets do %>
<tr>
<td>
<span class="text-sm font-mono opacity-70">
<%= case packet.received_at do %>
<% %DateTime{} = dt -> %>
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S")}
<% dt when is_binary(dt) -> %>
<%= case DateTime.from_iso8601(dt) do %>
<% {:ok, parsed_dt, _} -> %>
{Calendar.strftime(parsed_dt, "%Y-%m-%d %H:%M:%S")}
<% _ -> %>
{dt}
<% end %>
<% _ -> %>
N/A
<% end %>
</span>
</td>
<td>
<span class="text-sm font-medium">{packet.sender}</span>
</td>
<td>
<span class="badge badge-primary">
{packet.data_type}
</span>
</td>
<td>
<span class="text-sm font-mono">
<%= if String.length(packet.information_field || "") > 50 do %>
<span title={packet.information_field}>
{String.slice(packet.information_field, 0, 50)}...
</span>
<% else %>
{packet.information_field}
<% end %>
</span>
</td>
<td>
<span class="text-xs font-mono opacity-70">
{packet.path}
</span>
</td>
<td>
<span class="text-xs font-mono opacity-70">
<%= if packet.device_model || packet.device_vendor do %>
{[packet.device_model, packet.device_vendor]
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")}
<% else %>
{Map.get(
packet,
:device_identifier,
Map.get(packet, "device_identifier", "")
)}
<% end %>
</span>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<%= if length(@all_packets) == 0 do %>
<div class="mt-8 text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100">
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="text-center py-12">
<div class="flex justify-center mb-4">
<svg class="w-16 h-16 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -89,25 +109,27 @@
/>
</svg>
</div>
<h3 class="mt-2 text-sm font-medium text-gray-900">No packets found for {@callsign}</h3>
<p class="mt-1 text-sm text-gray-500">
<h3 class="text-lg font-medium mb-2">No packets found for {@callsign}</h3>
<p class="opacity-70">
Packets will appear here as they are received live or if there are stored packets for this callsign.
</p>
</div>
<% end %>
<div class="mt-4 px-4 py-3 bg-gray-50 border-t border-gray-200 rounded-b-lg">
<div class="flex items-center justify-between text-sm text-gray-500">
<div class="flex items-center space-x-4">
<span>
Live packets: <span class="font-medium text-gray-900">{length(@live_packets)}</span>
</span>
<span>
Stored packets: <span class="font-medium text-gray-900">{length(@packets)}</span>
</span>
<span>
Total: <span class="font-medium text-gray-900">{length(@all_packets)}/100</span>
</span>
<div class="card bg-base-100 shadow-xl mt-4">
<div class="card-body">
<div class="flex items-center justify-between text-sm">
<div class="flex items-center space-x-4">
<span>
Live packets: <span class="font-medium">{length(@live_packets)}</span>
</span>
<span>
Stored packets: <span class="font-medium">{length(@packets)}</span>
</span>
<span>
Total: <span class="font-medium">{length(@all_packets)}/100</span>
</span>
</div>
</div>
</div>
</div>

View file

@ -1,132 +1,158 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
<div class="mt-6 bg-white shadow-sm rounded-lg overflow-hidden">
<.table id="packets" rows={@packets}>
<:col :let={packet} label="Sender">
<.link
navigate={
~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"
}
class="text-blue-600 hover:text-blue-800 font-medium"
>
{Map.get(packet, :sender, Map.get(packet, "sender", ""))}
</.link>
</:col>
<:col :let={packet} label="SSID">
<span class="text-sm text-gray-900">
{Map.get(packet, :ssid, Map.get(packet, "ssid", ""))}
</span>
</:col>
<:col :let={packet} label="Base Callsign">
<.link
navigate={
~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"
}
class="text-blue-600 hover:text-blue-800 font-medium"
>
{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}
</.link>
</:col>
<:col :let={packet} label="Data Type">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{Map.get(packet, :data_type, Map.get(packet, "data_type", ""))}
</span>
</:col>
<:col :let={packet} label="Symbol">
<span class="text-xs text-gray-700 font-mono">
<% data = Map.get(packet, :data_extended) || %{}
raw_table = Map.get(data, :symbol_table_id) || Map.get(data, "symbol_table_id") || "/"
table = if raw_table in ["/", "\\", "]"], do: raw_table, else: "/"
code = Map.get(data, :symbol_code) || Map.get(data, "symbol_code") || ">" %>
{table}{code}
</span>
</:col>
<:col :let={packet} label="Path">
<span class="text-xs text-gray-500 font-mono">
{Map.get(packet, :path, Map.get(packet, "path", ""))}
</span>
</:col>
<:col :let={packet} label="Latitude">
<span class="text-xs text-gray-700 font-mono">
<% lat =
Map.get(packet, :lat) ||
if(
Map.has_key?(packet, :location) and not is_nil(packet.location) and
function_exported?(Aprsme.Packet, :lat, 1),
do: Aprsme.Packet.lat(packet),
else: nil
) ||
case Map.get(packet, :data_extended) do
%{} = data_ext ->
case Map.get(data_ext, :latitude) || Map.get(data_ext, "latitude") do
{:ok, value} -> value
value -> value
end
<div class="container mx-auto max-w-7xl py-8">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Sender</th>
<th>SSID</th>
<th>Base Callsign</th>
<th>Data Type</th>
<th>Symbol</th>
<th>Path</th>
<th>Latitude</th>
<th>Longitude</th>
<th>Device</th>
</tr>
</thead>
<tbody>
<%= for packet <- @packets do %>
<tr>
<td>
<.link
navigate={
~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"
}
class="link link-primary font-medium"
>
{Map.get(packet, :sender, Map.get(packet, "sender", ""))}
</.link>
</td>
<td>
<span class="text-sm">
{Map.get(packet, :ssid, Map.get(packet, "ssid", ""))}
</span>
</td>
<td>
<.link
navigate={
~p"/packets/#{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}"
}
class="link link-primary font-medium"
>
{Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))}
</.link>
</td>
<td>
<span class="badge badge-primary">
{Map.get(packet, :data_type, Map.get(packet, "data_type", ""))}
</span>
</td>
<td>
<span class="text-xs font-mono">
<% data = Map.get(packet, :data_extended) || %{}
_ ->
nil
end %>
<%= if not is_nil(lat) do %>
<%= if is_float(lat) do %>
{:io_lib.format("~.6f", [lat]) |> List.to_string()}
<% else %>
<%= if is_binary(lat) do %>
{Regex.replace(~r/(\d+\.\d{1,6})\d*/, lat, "\\1")}
<% else %>
{lat}
<% end %>
<% end %>
<% else %>
""
<% end %>
</span>
</:col>
<:col :let={packet} label="Longitude">
<span class="text-xs text-gray-700 font-mono">
<% lon =
Map.get(packet, :lon) ||
if(
Map.has_key?(packet, :location) and not is_nil(packet.location) and
function_exported?(Aprsme.Packet, :lon, 1),
do: Aprsme.Packet.lon(packet),
else: nil
) ||
case Map.get(packet, :data_extended) do
%{} = data_ext ->
case Map.get(data_ext, :longitude) || Map.get(data_ext, "longitude") do
{:ok, value} -> value
value -> value
end
raw_table =
Map.get(data, :symbol_table_id) || Map.get(data, "symbol_table_id") || "/"
_ ->
nil
end %>
<%= if not is_nil(lon) do %>
<%= if is_float(lon) do %>
{:io_lib.format("~.6f", [lon]) |> List.to_string()}
<% else %>
<%= if is_binary(lon) do %>
{Regex.replace(~r/(\d+\.\d{1,6})\d*/, lon, "\\1")}
<% else %>
{lon}
<% end %>
table = if raw_table in ["/", "\\", "]"], do: raw_table, else: "/"
code = Map.get(data, :symbol_code) || Map.get(data, "symbol_code") || ">" %>
{table}{code}
</span>
</td>
<td>
<span class="text-xs font-mono opacity-70">
{Map.get(packet, :path, Map.get(packet, "path", ""))}
</span>
</td>
<td>
<span class="text-xs font-mono">
<% lat =
Map.get(packet, :lat) ||
if(
Map.has_key?(packet, :location) and not is_nil(packet.location) and
function_exported?(Aprsme.Packet, :lat, 1),
do: Aprsme.Packet.lat(packet),
else: nil
) ||
case Map.get(packet, :data_extended) do
%{} = data_ext ->
case Map.get(data_ext, :latitude) || Map.get(data_ext, "latitude") do
{:ok, value} -> value
value -> value
end
_ ->
nil
end %>
<%= if not is_nil(lat) do %>
<%= if is_float(lat) do %>
{:io_lib.format("~.6f", [lat]) |> List.to_string()}
<% else %>
<%= if is_binary(lat) do %>
{Regex.replace(~r/(\d+\.\d{1,6})\d*/, lat, "\\1")}
<% else %>
{lat}
<% end %>
<% end %>
<% else %>
""
<% end %>
</span>
</td>
<td>
<span class="text-xs font-mono">
<% lon =
Map.get(packet, :lon) ||
if(
Map.has_key?(packet, :location) and not is_nil(packet.location) and
function_exported?(Aprsme.Packet, :lon, 1),
do: Aprsme.Packet.lon(packet),
else: nil
) ||
case Map.get(packet, :data_extended) do
%{} = data_ext ->
case Map.get(data_ext, :longitude) || Map.get(data_ext, "longitude") do
{:ok, value} -> value
value -> value
end
_ ->
nil
end %>
<%= if not is_nil(lon) do %>
<%= if is_float(lon) do %>
{:io_lib.format("~.6f", [lon]) |> List.to_string()}
<% else %>
<%= if is_binary(lon) do %>
{Regex.replace(~r/(\d+\.\d{1,6})\d*/, lon, "\\1")}
<% else %>
{lon}
<% end %>
<% end %>
<% else %>
""
<% end %>
</span>
</td>
<td>
<span class="text-xs font-mono">
{Map.get(packet, :device_identifier, Map.get(packet, "device_identifier", ""))}
</span>
</td>
</tr>
<% end %>
<% else %>
""
<% end %>
</span>
</:col>
<:col :let={packet} label="Device">
<span class="text-xs text-gray-700 font-mono">
{Map.get(packet, :device_identifier, Map.get(packet, "device_identifier", ""))}
</span>
</:col>
</.table>
</tbody>
</table>
</div>
</div>
</div>
<%= if length(@packets) == 0 do %>
<div class="mt-8 text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100">
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="text-center py-12">
<div class="flex justify-center mb-4">
<svg class="w-16 h-16 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -135,8 +161,8 @@
/>
</svg>
</div>
<h3 class="mt-2 text-sm font-medium text-gray-900">No packets</h3>
<p class="mt-1 text-sm text-gray-500">Packets will appear here as they are received.</p>
<h3 class="text-lg font-medium mb-2">No packets</h3>
<p class="opacity-70">Packets will appear here as they are received.</p>
</div>
<% end %>
</div>

View file

@ -1,14 +1,14 @@
<%= if @weather_packet do %>
<div class="min-h-screen bg-gray-50">
<div class="bg-white shadow-sm border-b">
<div class="px-4 sm:px-6 lg:px-8">
<div class="min-h-screen bg-base-200">
<div class="bg-base-100 shadow-sm">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-4 md:flex md:items-center md:justify-between">
<div class="flex items-center">
<h1 class="text-xl font-bold leading-7 text-gray-900 sm:truncate sm:text-2xl">
<h1 class="text-xl font-bold leading-7 sm:truncate sm:text-2xl">
Weather Station
</h1>
<div class="ml-3 flex items-center space-x-3">
<span class="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-sm font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
<span class="badge badge-primary">
{@callsign}
</span>
<% {symbol_table_id, symbol_code} =
@ -39,22 +39,13 @@
</div>
<div class="mt-3 flex md:ml-4 md:mt-0">
<div class="flex space-x-2">
<.link
navigate={~p"/info/#{@callsign}"}
class="inline-flex items-center rounded-md bg-gray-100 px-3 py-1.5 text-sm font-semibold text-gray-700 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-200"
>
<.link navigate={~p"/info/#{@callsign}"} class="btn btn-outline btn-sm">
View info
</.link>
<.link
navigate={~p"/packets/#{@callsign}"}
class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
<.link navigate={~p"/packets/#{@callsign}"} class="btn btn-outline btn-sm">
View packets
</.link>
<.link
navigate={~p"/#{@callsign}"}
class="inline-flex items-center rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
<.link navigate={~p"/#{@callsign}"} class="btn btn-primary btn-sm">
View on map
</.link>
</div>
@ -62,9 +53,9 @@
</div>
</div>
</div>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
<div class="bg-white overflow-hidden shadow-sm rounded-lg border mb-6">
<div class="px-4 py-3">
<div class="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<% dt =
AprsmeWeb.TimeHelpers.to_datetime(
Map.get(@weather_packet, :inserted_at) || Map.get(@weather_packet, "inserted_at")
@ -72,7 +63,7 @@
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 text-blue-400"
class="h-4 w-4 text-primary"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
@ -85,13 +76,13 @@
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium text-gray-900">Weather Details</h3>
<h3 class="text-sm font-medium">Weather Details</h3>
</div>
</div>
<dl class="grid grid-cols-2 gap-3">
<div>
<dt class="text-xs font-medium text-gray-500">Last WX report</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Last WX report</dt>
<dd class="mt-1 text-sm font-semibold">
<%= if dt do %>
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")} ({AprsmeWeb.TimeHelpers.time_ago_in_words(
dt
@ -103,20 +94,20 @@
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Temperature</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Temperature</dt>
<dd class="mt-1 text-sm font-semibold">
{PacketUtils.get_weather_field(@weather_packet, :temperature)}°F
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Humidity</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Humidity</dt>
<dd class="mt-1 text-sm font-semibold">
{PacketUtils.get_weather_field(@weather_packet, :humidity)}%
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Wind</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Wind</dt>
<dd class="mt-1 text-sm font-semibold">
{PacketUtils.get_weather_field(@weather_packet, :wind_direction)}° @ {PacketUtils.get_weather_field(
@weather_packet,
:wind_speed
@ -124,38 +115,38 @@
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Gusts</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Gusts</dt>
<dd class="mt-1 text-sm font-semibold">
{PacketUtils.get_weather_field(@weather_packet, :wind_gust)} mph
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Pressure</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Pressure</dt>
<dd class="mt-1 text-sm font-semibold">
{PacketUtils.get_weather_field(@weather_packet, :pressure)} hPa
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Rain (1h)</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Rain (1h)</dt>
<dd class="mt-1 text-sm font-semibold">
{get_weather_field_zero(@weather_packet, :rain_1h)} in
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Rain (24h)</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Rain (24h)</dt>
<dd class="mt-1 text-sm font-semibold">
{get_weather_field_zero(@weather_packet, :rain_24h)} in
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500">Rain (since midnight)</dt>
<dd class="mt-1 text-sm font-semibold text-gray-900">
<dt class="text-xs font-medium opacity-70">Rain (since midnight)</dt>
<dd class="mt-1 text-sm font-semibold">
{get_weather_field_zero(@weather_packet, :rain_since_midnight)} in
</dd>
</div>
<div class="col-span-2">
<dt class="text-xs font-medium text-gray-500">Raw Packet</dt>
<dd class="mt-1 text-xs font-mono text-gray-900 break-all">
<dt class="text-xs font-medium opacity-70">Raw Packet</dt>
<dd class="mt-1 text-xs font-mono break-all">
<%= if is_binary(@weather_packet.raw_packet) do %>
{Aprsme.EncodingUtils.sanitize_string(@weather_packet.raw_packet)}
<% else %>
@ -164,19 +155,20 @@
</dd>
</div>
</dl>
<div class="mt-2 text-xs text-gray-500">
<div class="mt-2 text-xs opacity-70">
<strong>Raw comment:</strong> {@weather_packet.comment || @weather_packet["comment"]}
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow-sm rounded-lg border">
<div class="px-4 py-3">
<h3 class="text-sm font-medium text-gray-900 mb-2">Weather History Graphs</h3>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="text-sm font-medium mb-2">Weather History Graphs</h3>
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :temperature, nil)))) do %>
<div
id="temp-chart"
phx-hook="ChartJSTempChart"
data-weather-history={@weather_history_json}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -187,6 +179,7 @@
id="humidity-chart"
phx-hook="ChartJSHumidityChart"
data-weather-history={@weather_history_json}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -197,16 +190,7 @@
id="pressure-chart"
phx-hook="ChartJSPressureChart"
data-weather-history={@weather_history_json}
style="height:250px;"
>
<canvas></canvas>
</div>
<% end %>
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :wind_direction, nil)))) do %>
<div
id="wind-direction-chart"
phx-hook="ChartJSWindDirectionChart"
data-weather-history={@weather_history_json}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -214,9 +198,10 @@
<% end %>
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :wind_speed, nil)))) do %>
<div
id="wind-speed-chart"
phx-hook="ChartJSWindSpeedChart"
id="wind-chart"
phx-hook="ChartJSWindChart"
data-weather-history={@weather_history_json}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -227,6 +212,7 @@
id="rain-chart"
phx-hook="ChartJSRainChart"
data-weather-history={@weather_history_json}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -237,6 +223,7 @@
id="luminosity-chart"
phx-hook="ChartJSLuminosityChart"
data-weather-history={@weather_history_json}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
@ -253,10 +240,12 @@
</div>
</div>
<% else %>
<div class="w-full min-h-screen bg-slate-50 py-8 px-2 md:px-8 flex items-center justify-center">
<div class="w-full max-w-lg bg-white rounded-xl shadow-lg p-8 text-center">
<h1 class="text-2xl font-bold mb-2">Weather for {@callsign}</h1>
<div class="text-gray-600">No recent weather data available for this callsign.</div>
<div class="min-h-screen bg-base-200 flex items-center justify-center">
<div class="card bg-base-100 shadow-xl max-w-lg">
<div class="card-body text-center">
<h1 class="text-2xl font-bold mb-2">Weather for {@callsign}</h1>
<div class="opacity-70">No recent weather data available for this callsign.</div>
</div>
</div>
</div>
<% end %>