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
6 changes: 6 additions & 0 deletions database.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ export type Database = {
Row: {
ai_provider: string | null;
battery_changed_at: string | null;
battery_level: number | null;
dev_eui: string;
installed_at: string | null;
lat: number | null;
Expand All @@ -595,6 +596,7 @@ export type Database = {
Insert: {
ai_provider?: string | null;
battery_changed_at?: string | null;
battery_level?: number | null;
dev_eui: string;
installed_at?: string | null;
lat?: number | null;
Expand All @@ -611,6 +613,7 @@ export type Database = {
Update: {
ai_provider?: string | null;
battery_changed_at?: string | null;
battery_level?: number | null;
dev_eui?: string;
installed_at?: string | null;
lat?: number | null;
Expand Down Expand Up @@ -1057,6 +1060,7 @@ export type Database = {
created_at: string;
dev_eui: string;
id: number;
line_number: number | null;
people_count: number;
traffic_hour: string | null;
truck_count: number;
Expand All @@ -1068,6 +1072,7 @@ export type Database = {
created_at?: string;
dev_eui: string;
id?: number;
line_number?: number | null;
people_count?: number;
traffic_hour?: string | null;
truck_count?: number;
Expand All @@ -1079,6 +1084,7 @@ export type Database = {
created_at?: string;
dev_eui?: string;
id?: number;
line_number?: number | null;
people_count?: number;
traffic_hour?: string | null;
truck_count?: number;
Expand Down
64 changes: 64 additions & 0 deletions src/lib/components/BatteryLevel.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!-- Battery.svelte (Svelte 5) - Modern Design -->
<script lang="ts">
import MaterialIcon from './UI/icons/MaterialIcon.svelte';
import { Tooltip } from 'bits-ui';

// Props
let {
value = 50, // 0..100
size = 'medium',
showLabel = false, // show % text next to icon
charging = false, // optional charging bolt overlay
lowThreshold = 15, // % -> red
midThreshold = 40, // % -> amber
highThreshold = 70, // % -> yellow; above is green
ariaLabel = 'Battery level'
} = $props();

function getBatteryColor(): string {
if (value <= lowThreshold) return 'red';
if (value <= midThreshold) return 'orange';
if (value <= highThreshold) return 'yellow';
return 'green';
}

let batteryIconName = () => {
if (value >= 95) return 'battery_android_frame_full';
if (value >= 75) return 'battery_android_frame_6';
if (value >= 50) return 'battery_android_frame_5';
if (value >= 30) return 'battery_android_frame_4';
if (value >= 15) return 'battery_android_frame_3';
if (value >= 5) return 'battery_android_frame_2';
return 'battery_android_alert'; // very low battery
};
</script>

<div class="my-auto flex items-center" aria-label={ariaLabel}>
<Tooltip.Provider>
<Tooltip.Root delayDuration={1000}>
<Tooltip.Trigger class="flex items-center">
<MaterialIcon
name={batteryIconName()}
{size}
class={getBatteryColor() === 'green'
? 'text-green-500'
: getBatteryColor() === 'yellow'
? 'text-yellow-500'
: 'text-red-500'}
/>
{value}%
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<div
class="rounded-md bg-gray-800 px-2 py-1 text-sm text-white shadow-lg"
style="max-width: 200px;"
>
<h4 class="font-semibold">Battery Level</h4>
<p class="mt-1">
The current battery level of the device is at <strong>{value}%</strong>.
</p>
</div>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
</div>
5 changes: 3 additions & 2 deletions src/lib/components/StatsCard/StatsCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
key: string;
stats?: DeviceStats;
expandable?: boolean;
class: string;
};

let { key, stats = {}, expandable = true }: Props = $props();
let { key, stats = {}, expandable = true, class: className }: Props = $props();
let { min, max, avg, median, stdDev, count, lastReading, trend } = $derived(stats[key]);
let title = $derived($_(key));
let notation = $derived(nameToNotation(key));
Expand Down Expand Up @@ -51,7 +52,7 @@
</script>

<div
class="panel flex w-full flex-col items-center rounded-lg p-4 text-zinc-900 shadow-sm dark:text-white"
class="panel flex w-full flex-col items-center rounded-lg p-4 text-zinc-900 shadow-sm dark:text-white ${className}"
class:cursor-pointer={expandable}
role="button"
tabindex="0"
Expand Down
46 changes: 19 additions & 27 deletions src/routes/account/update-password/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { success } from '$lib/stores/toast.svelte.js';
import { formValidation } from '$lib/actions/formValidation';
import { enhance } from '$app/forms';
import { success } from '$lib/stores/toast.svelte.js';
import { formValidation } from '$lib/actions/formValidation';

let { data } = $props();
let form = $derived(data.form);
Expand All @@ -27,7 +27,7 @@ import { formValidation } from '$lib/actions/formValidation';
return () => {
loading = true;

// Client-side validation
// クライアント側バリデーション
if (!validatePasswords()) {
loading = false;
return;
Expand All @@ -38,43 +38,38 @@ import { formValidation } from '$lib/actions/formValidation';
await update();

if (result.type === 'success') {
// Clear the form fields on success
// 成功時にフォームをクリア
currentPassword = '';
newPassword = '';
confirmPassword = '';
success('Password updated successfully.');
success('パスワードが正常に更新されました。');
}
};
};
}
</script>

<svelte:head>
<title>Update Password | CropWatch</title>
<title>パスワードを更新 | CropWatch</title>
</svelte:head>

<div class="container mx-auto max-w-4xl px-4 py-8">
<div
class="bg-card-light dark:bg-card-dark text-text-light dark:text-text-dark rounded-lg p-6 shadow-md"
>
<h1 class="mb-6 text-2xl font-bold">Update Password</h1>
<h1 class="mb-6 text-2xl font-bold">パスワードを更新</h1>

{#if form?.success}
<div
class="mb-6 rounded-md bg-green-100 p-4 text-center text-green-700 dark:bg-green-900/30 dark:text-green-400"
>
<p>Your password has been successfully updated.</p>
<p>パスワードが正常に更新されました。</p>
</div>
{/if}

<form
method="POST"
use:enhance={handleSubmit()}
use:formValidation
class="form-container"
>
<form method="POST" use:enhance={handleSubmit()} use:formValidation class="form-container">
<div>
<label for="newPassword" class="mb-1 block text-sm font-medium">New Password</label>
<label for="newPassword" class="mb-1 block text-sm font-medium">新しいパスワード</label>
<input
type="password"
id="newPassword"
Expand All @@ -83,7 +78,7 @@ import { formValidation } from '$lib/actions/formValidation';
autocomplete="new-password"
required
minlength="8"
placeholder="Enter your new password"
placeholder="新しいパスワードを入力してください"
disabled={loading}
oninput={() => validatePasswords()}
class="text-text-light dark:text-text-dark focus:ring-primary w-full rounded-md border border-gray-300
Expand All @@ -92,14 +87,14 @@ import { formValidation } from '$lib/actions/formValidation';
/>
{#if !isStrongPassword && newPassword.length > 0}
<p class="mt-1 text-sm text-red-600 dark:text-red-400">
Password must be at least 8 characters long
パスワードは8文字以上である必要があります
</p>
{/if}
</div>

<div>
<label for="confirmPassword" class="mb-1 block text-sm font-medium"
>Confirm New Password</label
>新しいパスワード(確認用)</label
>
<input
type="password"
Expand All @@ -108,15 +103,15 @@ import { formValidation } from '$lib/actions/formValidation';
bind:value={confirmPassword}
autocomplete="new-password"
required
placeholder="Confirm your new password"
placeholder="新しいパスワードを再入力してください"
disabled={loading}
oninput={() => validatePasswords()}
class="text-text-light dark:text-text-dark focus:ring-primary w-full rounded-md border border-gray-300
bg-white px-3 py-2 focus:border-transparent
focus:ring-2 focus:outline-none dark:border-gray-700 dark:bg-gray-800"
/>
{#if !passwordsMatch && confirmPassword.length > 0}
<p class="mt-1 text-sm text-red-600 dark:text-red-400">Passwords don't match</p>
<p class="mt-1 text-sm text-red-600 dark:text-red-400">パスワードが一致しません</p>
{/if}
</div>

Expand All @@ -131,13 +126,10 @@ import { formValidation } from '$lib/actions/formValidation';
<div>
<button
type="submit"
class="bg-primary hover:bg-primary-hover w-full rounded px-4 py-2 font-medium text-white transition-colors duration-200 disabled:opacity-50"
disabled={loading ||
!passwordsMatch ||
!isStrongPassword ||
!newPassword}
style="background: green;"
disabled={loading || !passwordsMatch || !isStrongPassword || !newPassword}
>
{loading ? 'Updating Password...' : 'Update Password'}
{loading ? 'パスワードを更新中...' : 'パスワードを更新'}
</button>
</div>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@
let renderingVisualization = $state(false); // Local rendering state for visualization

// Derived properties
const {
deviceTypeName,
temperatureChartVisible,
humidityChartVisible,
moistureChartVisible,
co2ChartVisible,
phChartVisible
} = $derived(getDeviceDetailDerived(device, dataType, latestData));
// const {
// deviceTypeName,
// temperatureChartVisible,
// humidityChartVisible,
// moistureChartVisible,
// co2ChartVisible,
// phChartVisible
// } = $derived(getDeviceDetailDerived(device, dataType, latestData));
let channel: RealtimeChannel | undefined = $state(undefined); // Channel for realtime updates
// Track last realtime update timestamp for stale detection
let lastRealtimeUpdate = $state<number>(Date.now());
Expand Down Expand Up @@ -409,6 +409,7 @@

// Cleanup on destroy
import { onDestroy } from 'svelte';
import BatteryLevel from '$lib/components/BatteryLevel.svelte';
onDestroy(() => {
teardownRealtime();
if (staleCheckIntervalId) clearInterval(staleCheckIntervalId);
Expand All @@ -422,6 +423,10 @@
</svelte:head>

<Header {device} {basePath}>
{#if device.battery_level !== null}
<BatteryLevel value={device.battery_level} size={28} showLabel />
{/if}

<!-- Data range selector on large screen -->
<div class="hidden border-r border-neutral-400 pl-4 lg:block">
<DateRangeSelector
Expand Down Expand Up @@ -526,7 +531,7 @@
{:else if device.cw_device_type?.data_table_v2 === 'cw_relay_data'}
<RelayControl {device} />
{:else}
<div class="mb-8">
<!-- <div class="mb-8">
<h2>{$_('Stats Summary')}</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
{#each numericKeys as key (key)}
Expand All @@ -535,6 +540,27 @@
{/if}
{/each}
</div>
</div> -->

<div class="mb-8">
<h2>{$_('Stats Summary')}</h2>

<!-- Auto-fit makes items expand when a row isn't full -->
<div
class="
/* ~max
4
cols
on normal desktops */ grid grid-cols-[repeat(auto-fit,minmax(260px,1fr))] items-stretch gap-4 md:grid-cols-[repeat(auto-fit,minmax(300px,1fr))]
xl:grid-cols-[repeat(auto-fit,minmax(340px,1fr))]
"
>
{#each numericKeys as key (key)}
{#if stats[key]}
<StatsCard {stats} {key} class="h-full w-full" />
{/if}
{/each}
</div>
</div>
{/if}
</section>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import Button from '$lib/components/UI/buttons/Button.svelte';
import MaterialIcon from '$lib/components/UI/icons/MaterialIcon.svelte';
import type { DeviceWithType } from '$lib/models/Device';
import type { Snippet } from 'svelte';
import { _ } from 'svelte-i18n';
Expand All @@ -19,6 +20,13 @@
>
<h1 class="flex w-full flex-1 flex-row items-center gap-4 md:flex-row md:items-end">
<div class="w-full text-2xl font-semibold text-gray-900 lg:text-3xl dark:text-gray-100">
<Button onclick={() => history.back()} class="p-0">
<MaterialIcon
name="arrow_back"
size="medium"
class="text-gray-500 hover:text-gray-700 dark:text-gray-200 dark:hover:text-gray-100"
/>
</Button>
{device.name}
</div>
</h1>
Expand Down
Loading