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
4 changes: 0 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@
"vitest": "^3.0.0"
},
"dependencies": {
"@cropwatchdevelopment/cwui": "^0.0.20",
"@layerstack/tailwind": "2.0.0-next.2",
"@layerstack/utils": "^1.0.0",
"@mdi/js": "^7.4.47",
"@stencil/store": "^2.1.3",
"@stripe/stripe-js": "^7.4.0",
Expand All @@ -90,7 +87,6 @@
"pdfkit": "^0.17.1",
"stripe": "^18.3.0",
"svelte-i18n": "^4.0.1",
"svelte-ux": "^1.0.4",
"swagger-ui": "^5.21.0",
"utf-8-validate": "^6.0.5"
},
Expand Down
864 changes: 0 additions & 864 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@

@import 'tailwindcss';

@import '@layerstack/tailwind/core.css';
@import '@layerstack/tailwind/utils.css';
@import '@layerstack/tailwind/themes/all.css';

@source '../../node_modules/svelte-ux/dist';
/* Removed @layerstack/tailwind and svelte-ux source imports; using local styles only */
@plugin '@tailwindcss/typography';

/* Define color variables for light mode */
Expand Down
27 changes: 17 additions & 10 deletions src/lib/components/GlobalSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { _ } from 'svelte-i18n';
import { onMount } from 'svelte';
import LanguageSelector from './UI/form/LanguageSelector.svelte';
import { globalLoading, startLoading } from '$lib/stores/loadingStore';
import { startLoading } from '$lib/stores/loadingStore';
import ThemeToggle from './theme/ThemeToggle.svelte';
import { goto } from '$app/navigation';

Expand Down Expand Up @@ -137,7 +137,7 @@

<!-- Sidebar -->
<aside
class="fixed top-0 left-0 z-50 flex flex-col border-r transition-all duration-300 ease-in-out
class="fixed top-0 left-0 z-50 flex flex-col overflow-x-hidden border-r transition-all duration-300 ease-in-out
{getDarkMode() ? 'border-slate-700/30 bg-slate-800/95' : 'border-gray-200/30 bg-slate-200'}
shadow-lg backdrop-blur-sm"
style="top: 73px;
Expand All @@ -158,8 +158,8 @@
</h2>
<button
onclick={() => {
// Toggle sidebar only (no navigation) – don't trigger global loading overlay
sidebarStore.toggle();
startLoading();
}}
class="rounded-lg p-2 transition-all duration-200 hover:scale-105 {getDarkMode()
? 'text-slate-400 hover:bg-slate-700/50 hover:text-white'
Expand All @@ -176,27 +176,32 @@
<ul class="space-y-1">
{#if !sidebarStore.isOpen}
<li class="mx-3 border-t {getDarkMode() ? 'border-slate-500/30' : 'border-gray-200/30'}">
<a
<button
type="button"
onclick={() => {
// Opening sidebar (no route change) – don't show global loader
sidebarStore.toggle();
startLoading();
}}
class="flex items-center justify-center rounded-lg p-2 transition-all duration-200 hover:scale-105
class="flex w-full items-center justify-center rounded-lg p-2 transition-all duration-200 hover:scale-105
{getDarkMode()
? 'text-slate-400 hover:bg-slate-700/50 hover:text-white'
: 'text-gray-500 hover:bg-gray-100 hover:text-gray-900'}"
aria-label="Open sidebar"
>
<MaterialIcon name="menu" size="medium" />
</a>
</button>
</li>
{/if}
{#each navigationItems as item, index}
<li>
<a
href={item.href}
onclick={() => {
// Only trigger loading if we're navigating to a different route
if ($page.url.pathname !== item.href) {
startLoading();
}
sidebarStore.close();
startLoading();
}}
class="decoration-none flex items-center gap-3 rounded-lg px-3 py-2 no-underline transition-all duration-200 hover:scale-105
{isActiveRoute(item.matcher)
Expand Down Expand Up @@ -245,10 +250,12 @@
<a
href="/app/account-settings/general"
onclick={() => {
if ($page.url.pathname !== '/app/account-settings/general') {
startLoading();
}
sidebarStore.close();
startLoading();
}}
class="decoration-none flex items-center gap-3 rounded-lg px-3 py-2 no-underline transition-all duration-200 hover:scale-105
class="decoration-none flex items-center gap-3 rounded-lg px-3 py-2 no-underline transition-all duration-200
{isActiveRoute('/app/account-settings/general')
? getDarkMode()
? 'bg-emerald-600/30 text-emerald-400'
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/StatsCard/StatsCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { formatNumber, getTextColorByKey } from '$lib/utilities/stats';
import { mdiArrowDownBold, mdiArrowUpBold, mdiMinus } from '@mdi/js';
import { _ } from 'svelte-i18n';
import { Icon } from 'svelte-ux';
import Icon from '$lib/components/ui/base/Icon.svelte';
import MaterialIcon from '../UI/icons/MaterialIcon.svelte';

type Props = {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/UI/dashboard/DashboardCard.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { Icon } from 'svelte-ux';
import Icon from '$lib/components/ui/base/Icon.svelte';
import { goto } from '$app/navigation';
import { mdiAlert, mdiCheck, mdiClose, mdiClockOutline } from '@mdi/js';
import type { Location } from '$lib/models/Location';
Expand Down
121 changes: 41 additions & 80 deletions src/lib/components/UI/dashboard/DataRowItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
import { nameToEmoji } from '$lib/utilities/NameToEmoji';
import { formatNumber } from '$lib/utilities/stats';
import { mdiArrowRight } from '@mdi/js';
import type { Snippet } from 'svelte';
import { _ } from 'svelte-i18n';
import { Collapse } from 'svelte-ux';
import Collapse from '$lib/components/ui/base/Collapse.svelte';
import Button from './components/Button.svelte';

// Extend the Device type to include latestData
interface DeviceWithLatestData extends Device {
latestData: Record<string, any>;
cw_device_type: {
Expand All @@ -34,98 +32,49 @@
location?: Location;
isActive?: boolean;
detailHref?: string;
children?: Snippet;
children?: any; // snippet passed by parent
}>();

// Use the isActive prop directly from the parent component
// This simplifies the component and ensures consistent active status logic
// Start with a neutral 'loading' state until we get confirmation from the timer logic
let isActive = $derived(
externalIsActive !== undefined
? externalIsActive === null
? null
: Boolean(externalIsActive)
: null
);

// Track whether we've received a definitive status update
let statusConfirmed = $state(false);

// Update statusConfirmed when we get a definitive status
$effect(() => {
// Only set statusConfirmed to true when we have a non-null status
if (externalIsActive !== undefined && externalIsActive !== null) {
statusConfirmed = true;
}
if (externalIsActive !== undefined && externalIsActive !== null) statusConfirmed = true;
});

// Log the active status for debugging
// $effect(() => {
// //console.log(
// `[DataRowItem] Device ${device.name} (${device.dev_eui}) isActive: ${isActive}, statusConfirmed: ${statusConfirmed}, cw_device_type: ${device.cw_device_type.name}`
// );
// });

// Determine the primary and secondary data keys based on device type - using reactive declarations
let primaryDataKey = $derived(device.cw_device_type.primary_data_v2);
let secondaryDataKey = $derived(device.cw_device_type.secondary_data_v2);

// Get the data values - using reactive declarations so they update when latestData changes
let primaryValue = $derived(device.latestData?.[primaryDataKey]);
let secondaryValue = $derived(device.latestData?.[secondaryDataKey]);

// Get the notations - using reactive declarations
let primaryNotation = $derived(device.cw_device_type.primary_data_notation || '°C');
let secondaryNotation = $derived(device.cw_device_type.secondary_data_notation || '%');

// Add a reactive effect to log when data changes
// $effect(() => {
// //console.log(`DataRowItem: ${device.name} (${device.dev_eui}) data updated:`, {
// primaryKey: primaryDataKey,
// primaryValue,
// secondaryKey: secondaryDataKey,
// secondaryValue,
// timestamp: device.latestData?.created_at
// });
// });

let localStorageOpenState = localStorage.getItem(`${device.dev_eui}-collapseState`);
let localStorageOpenState =
typeof localStorage !== 'undefined'
? localStorage.getItem(`${device.dev_eui}-collapseState`)
: null;
let defaultCollapse = $state(localStorageOpenState ? JSON.parse(localStorageOpenState) : false);

function collapseStateChange(e: CustomEvent) {
defaultCollapse = e.detail.open;
localStorage.setItem(`${device.dev_eui}-collapseState`, JSON.stringify(e.detail.open));
if (typeof localStorage !== 'undefined')
localStorage.setItem(`${device.dev_eui}-collapseState`, JSON.stringify(e.detail.open));
}

// $effect(() => {
// $inspect('device', device, 'latestData', device.latestData);
// });
</script>

<Collapse
classes={{
root: 'mb-1 bg-gray-200 dark:bg-gray-800/30 w-full ',
icon: 'text-gray-400 dark:text-gray-500 data-[open=true]:rotate-90'
}}
open={defaultCollapse}
on:change={(e) => collapseStateChange(e)}
>
<!-- Use a four-state color system for better UX:
- Blue-gray (loading): Initial state before status is confirmed
- Blue (neutral): When we don't have data
- Green: When device is confirmed active with recent data
- Red: When device is confirmed inactive (data too old)
-->
<div slot="trigger" class="relative flex flex-1">
<!-- Status indicator -->
{#snippet triggerSnippet()}
<div class="relative flex flex-1">
<div
class="absolute top-0 bottom-0 left-0 my-1 w-1.5 rounded-full opacity-70"
class:bg-blue-300={!statusConfirmed || isActive === null}
class:bg-blue-400={statusConfirmed && !device.latestData?.created_at}
class:bg-green-500={statusConfirmed && isActive}
class:bg-red-500={statusConfirmed && !isActive && device.latestData?.created_at}
></div>

<div class="my-1 mr-2 ml-2 flex-1 border-r-2">
<div class="flex flex-col text-base">
<div class="justify-left flex flex-row pl-0">
Expand All @@ -151,7 +100,6 @@
</span>
</div>
</div>

{#if secondaryDataKey}
<span class="flex flex-grow-[0.2]"></span>
<div class="flex items-center">
Expand All @@ -163,9 +111,9 @@
class="flex flex-nowrap items-baseline text-lg leading-tight font-bold text-gray-900 dark:text-white"
>
<span>{formatNumber({ key: secondaryDataKey, value: secondaryValue })}</span>
<span class="text-accent-700 dark:text-accent-400 ml-0.5 text-xs font-normal">
{secondaryNotation}
</span>
<span class="text-accent-700 dark:text-accent-400 ml-0.5 text-xs font-normal"
>{secondaryNotation}</span
>
</span>
</div>
</div>
Expand All @@ -175,18 +123,31 @@
</div>
</div>
</div>
{/snippet}

{#snippet collapseChildren()}
<div class="space-y-2">
{#if children}
{@render children()}
{/if}
{#if detailHref || location}
<Button
text={$_('View Details')}
iconPath={mdiArrowRight}
onClick={() =>
goto(`/app/dashboard/location/${device.location_id}/devices/${device.dev_eui}`)}
/>
{/if}
</div>
{/snippet}

<!-- Content from children snippet -->
{#if children}
{@render children()}
{/if}

{#if detailHref || location}
<Button
text={$_('View Details')}
iconPath={mdiArrowRight}
onClick={() =>
goto(`/app/dashboard/location/${device.location_id}/devices/${device.dev_eui}`)}
/>
{/if}
</Collapse>
<Collapse
classes={{
root: 'mb-1 bg-gray-200 dark:bg-gray-800/30 w-full ',
icon: 'text-gray-400 dark:text-gray-500 data-[open=true]:rotate-90'
}}
open={defaultCollapse}
on:change={(e) => collapseStateChange(e)}
trigger={triggerSnippet}
children={collapseChildren}
/>
4 changes: 2 additions & 2 deletions src/lib/components/UI/dashboard/DeviceDataList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import { nameToEmoji } from '$lib/utilities/NameToEmoji';
import { nameToNotation } from '$lib/utilities/NameToNotation';
import { formatNumber } from '$lib/utilities/stats';
import { DurationUnits } from '@layerstack/utils';
import { _ } from 'svelte-i18n';
import { Duration } from 'svelte-ux';
import Duration from '$lib/components/ui/base/Duration.svelte';
import { DurationUnits } from '$lib/utilities/duration';

// Extend the Device type to include latestData
interface DeviceWithLatestData extends Device {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/UI/dashboard/LocationsPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { mdiClose, mdiMagnify } from '@mdi/js';
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { Icon } from 'svelte-ux';
import Icon from '$lib/components/ui/base/Icon.svelte';

// Props
export let locations: LocationWithCount[] = [];
Expand Down
Empty file.
Empty file.
36 changes: 36 additions & 0 deletions src/lib/components/UI/primitives/Icon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
// Lightweight Icon component replacing svelte-ux Icon
interface Props {
path?: string;
data?: string;
size?: string | number;
class?: string;
title?: string;
}
let {
path = undefined,
data = undefined,
size = '1em',
class: className = '',
title
}: Props = $props();
let d = $derived(path || data || '');
let width = $derived(typeof size === 'number' ? `${size}px` : size);
let height = $derived(width);
</script>

<svg
{title}
role="img"
aria-hidden={title ? undefined : true}
viewBox="0 0 24 24"
{width}
{height}
class={className}
fill="currentColor"
>
{#if d}
<path {d} />
{/if}
<slot />
</svg>
Loading