refactor(alert-settings): integrate AppShell layout and update cache version
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Has been cancelled

- Replace custom page layout with AppShell component for consistent UI
- Remove unused PageHeader and UpdateAlertConditionDto imports
- Update service worker cache names and version.json to new timestamp
This commit is contained in:
2026-05-11 22:19:30 +03:30
parent 676f1373c1
commit 9da69c96b1
20 changed files with 1025 additions and 172 deletions

View File

@@ -1,13 +1,14 @@
"use client"
import { useEffect, useState, useCallback, Suspense } from 'react'
import { useSearchParams } from 'next/navigation'
import { api, AlertConditionDto, UpdateAlertConditionDto, CreateAlertConditionDto, CreateAlertRuleRequest } from '@/lib/api'
import { api, AlertConditionDto, CreateAlertConditionDto, CreateAlertRuleRequest } from '@/lib/api'
import { Bell, Plus, AlertTriangle } from 'lucide-react'
import Loading from '@/components/Loading'
import { PageHeader, EmptyState } from '@/components/common'
import { EmptyState } from '@/components/common'
import { confirmDialog } from '@/components/utils'
import { AlertListItem } from '@/components/alert-settings/AlertListItem'
import { AlertFormModal } from '@/components/alert-settings/AlertFormModal'
import { AppShell } from '@/components/layout'
function AlertSettingsContent() {
const searchParams = useSearchParams()
@@ -102,39 +103,36 @@ function AlertSettingsContent() {
if (loading) return <Loading message="در حال بارگذاری تنظیمات هشدار..." />
return (
<div className="min-h-screen p-4 md:p-6 bg-gray-50">
<div className="max-w-7xl mx-auto space-y-6">
<PageHeader
icon={Bell}
title="تنظیمات هشدارها"
subtitle="مدیریت شرایط و هشدارهای دستگاه"
iconGradient="from-orange-500 to-red-600"
action={
<button onClick={openCreate}
className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white rounded-xl transition-all duration-200 font-medium shadow-md hover:shadow-lg">
<Plus className="w-5 h-5" /> افزودن هشدار
</button>
}
/>
<div className="bg-white rounded-2xl shadow-md border border-gray-200 overflow-hidden">
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">لیست هشدارها ({alerts.length})</h2>
</div>
{alerts.length === 0 ? (
<EmptyState icon={AlertTriangle} message="هیچ هشداری ثبت نشده است"
action={<button onClick={openCreate} className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg transition-colors"><Plus className="w-4 h-4" /> افزودن اولین هشدار</button>}
/>
) : (
<div className="divide-y divide-gray-200">
{alerts.map(alert => (
<AlertListItem key={alert.id} alert={alert} onEdit={openEdit} onDelete={handleDelete} onToggleActive={toggleActive} />
))}
<>
<AppShell
pageTitle="تنظیمات هشدارها"
pageIcon={Bell}
iconGradient="from-orange-500 to-red-600"
>
<div className="space-y-6">
<div className="bg-white rounded-2xl shadow-md border border-gray-200 overflow-hidden">
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900">لیست هشدارها ({alerts.length})</h2>
<button onClick={openCreate}
className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white rounded-xl transition-all duration-200 font-medium shadow-md hover:shadow-lg">
<Plus className="w-5 h-5" /> افزودن هشدار
</button>
</div>
)}
{alerts.length === 0 ? (
<EmptyState icon={AlertTriangle} message="هیچ هشداری ثبت نشده است"
action={<button onClick={openCreate} className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg transition-colors"><Plus className="w-4 h-4" /> افزودن اولین هشدار</button>}
/>
) : (
<div className="divide-y divide-gray-200">
{alerts.map(alert => (
<AlertListItem key={alert.id} alert={alert} onEdit={openEdit} onDelete={handleDelete} onToggleActive={toggleActive} />
))}
</div>
)}
</div>
</div>
</div>
</AppShell>
<AlertFormModal
isOpen={modal.open}
@@ -148,7 +146,7 @@ function AlertSettingsContent() {
onRemoveRule={removeRule}
onUpdateRule={updateRule}
/>
</div>
</>
)
}

View File

@@ -5,10 +5,11 @@ import { api } from '@/lib/api'
import { getCurrentPersianYear } from '@/lib/date/persian-date'
import { Calendar as CalendarIcon, Database, TrendingUp } from 'lucide-react'
import Loading from '@/components/Loading'
import { PageHeader, BackLink, Card } from '@/components/common'
import { Card } from '@/components/common'
import { YearSelector } from '@/components/calendar'
import { MonthCard } from '@/components/cards'
import { StatsCard } from '@/components/cards'
import { AppShell } from '@/components/layout'
const monthNames = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
@@ -75,16 +76,12 @@ export default function CalendarPage() {
}
return (
<div className="min-h-screen p-4 md:p-6">
<AppShell
pageTitle="انتخاب سال و ماه"
pageIcon={CalendarIcon}
iconGradient="from-green-500 to-green-600"
>
<div className="max-w-6xl mx-auto">
{/* Header */}
<BackLink href="/" label="بازگشت به صفحه اصلی" />
<PageHeader
icon={CalendarIcon}
title="انتخاب سال و ماه"
iconGradient="from-green-500 to-green-600"
/>
{/* Main Card */}
<Card padding="lg">
{/* Year Selector */}
@@ -130,6 +127,6 @@ export default function CalendarPage() {
</div>
</Card>
</div>
</div>
</AppShell>
)
}

View File

@@ -6,14 +6,14 @@ import { persianToGregorian, getCurrentPersianDay, getCurrentPersianYear, getCur
import { formatPersianDate, ensureDateFormat } from '@/lib/format/persian-date'
import { TABS, TabType } from '@/features/daily-report'
import { detectDataGaps } from '@/features/daily-report/utils'
import { BarChart3, Bell, CalendarIcon, ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { BarChart3, CalendarIcon, ChevronRight } from 'lucide-react'
import Loading from '@/components/Loading'
import { SummaryTab } from '@/components/daily-report'
import { fetchForecastWeather, isToday as checkIsToday, WeatherData } from '@/features/weather'
import { Tabs, PageHeader, Button } from '@/components/common'
import { Tabs, Button } from '@/components/common'
import { DateNavigation } from '@/components/navigation'
import { usePullToRefresh } from '@/hooks/usePullToRefresh'
import { AppShell } from '@/components/layout'
// Lazy load heavy components
const ChartsTab = lazy(() => import('@/components/daily-report/ChartsTab').then(m => ({ default: m.ChartsTab })))
@@ -201,50 +201,43 @@ function DailyReportContent() {
if (!selectedDate) {
return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="text-center">
<CalendarIcon className="w-12 h-12 text-red-500 mx-auto mb-4" />
<div className="text-lg text-red-600 mb-4">تاریخ انتخاب نشده است</div>
<Button
onClick={goToCalendar}
variant="outline"
icon={ChevronRight}
>
بازگشت به تقویم
</Button>
<AppShell
pageTitle="گزارش روزانه"
pageIcon={BarChart3}
iconGradient="from-indigo-500 to-purple-600"
>
<div className="flex items-center justify-center p-4">
<div className="text-center">
<CalendarIcon className="w-12 h-12 text-red-500 mx-auto mb-4" />
<div className="text-lg text-red-600 mb-4">تاریخ انتخاب نشده است</div>
<Button
onClick={goToCalendar}
variant="outline"
icon={ChevronRight}
>
بازگشت به تقویم
</Button>
</div>
</div>
</div>
</AppShell>
)
}
return (
<div className="min-h-screen p-4 md:p-6">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<PageHeader
icon={BarChart3}
title="گزارش روزانه"
iconGradient="from-indigo-500 to-purple-600"
action={
<Link
href={`/alert-settings?deviceId=${deviceId}`}
className="flex items-center gap-2 px-4 py-2.5 bg-white border-2 border-orange-300 hover:border-orange-500 hover:bg-orange-50 text-orange-700 hover:text-orange-800 rounded-xl transition-all duration-200 font-medium shadow-sm hover:shadow-md"
>
<Bell className="w-5 h-5" />
<span className="hidden sm:inline">تنظیمات هشدار</span>
</Link>
}
/>
<AppShell
pageTitle="گزارش روزانه"
pageIcon={BarChart3}
pageInfo={selectedDate}
iconGradient="from-indigo-500 to-purple-600"
>
<div className="space-y-6">
{/* Date Navigation Buttons */}
{selectedDate && (
<DateNavigation
selectedDate={selectedDate}
onPrevious={goToPreviousDay}
onNext={goToNextDay}
onCalendar={goToCalendar}
/>
)}
<DateNavigation
selectedDate={selectedDate}
onPrevious={goToPreviousDay}
onNext={goToNextDay}
onCalendar={goToCalendar}
/>
{/* Tabs */}
<Tabs
@@ -273,7 +266,7 @@ function DailyReportContent() {
}}
</Tabs>
</div>
</div>
</AppShell>
)
}

View File

@@ -5,9 +5,10 @@ import { api } from '@/lib/api'
import { getCurrentPersianYear, getCurrentPersianMonth, getPersianMonthStartWeekday, getPersianMonthDays } from '@/lib/date/persian-date'
import { Calendar as CalendarIcon, Database } from 'lucide-react'
import Loading from '@/components/Loading'
import { PageHeader, BackLink, Card } from '@/components/common'
import { Card } from '@/components/common'
import { WeekdayHeaders } from '@/components/calendar'
import { CalendarDayCell, StatsCard } from '@/components/cards'
import { AppShell } from '@/components/layout'
const monthNames = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
@@ -71,16 +72,12 @@ function DayDetailsContent() {
}
return (
<div className="min-h-screen p-4 md:p-6">
<AppShell
pageTitle={`${monthNames[month - 1]} ${year}`}
pageIcon={CalendarIcon}
iconGradient="from-green-500 to-green-600"
>
<div className="max-w-6xl mx-auto">
{/* Header */}
<BackLink href={`/calendar?deviceId=${deviceId}`} label="بازگشت به تقویم" />
<PageHeader
icon={CalendarIcon}
title={`${monthNames[month - 1]} ${year}`}
iconGradient="from-green-500 to-green-600"
/>
{/* Calendar Grid */}
<Card className="overflow-hidden" padding="none">
{/* Weekday Headers */}
@@ -138,7 +135,7 @@ function DayDetailsContent() {
/>
</div>
</div>
</div>
</AppShell>
)
}

View File

@@ -1,11 +1,11 @@
"use client"
import { useEffect, useState, useCallback } from 'react'
import { api, DeviceSettingsDto } from '@/lib/api'
import { Settings, Thermometer, Wind, Sun, Droplets, Save, Loader2, AlertCircle, Bell } from 'lucide-react'
import Link from 'next/link'
import { Settings, Thermometer, Wind, Sun, Droplets, Save, Loader2, AlertCircle } from 'lucide-react'
import Loading from '@/components/Loading'
import { PageHeader, BackLink, ErrorMessage, SuccessMessage, EmptyState, Card } from '@/components/common'
import { ErrorMessage, SuccessMessage, EmptyState, Card } from '@/components/common'
import { SettingsSection, SettingsInputGroup } from '@/components/settings'
import { AppShell } from '@/components/layout'
function useQueryParam(name: string) {
if (typeof window === 'undefined') return null as string | null
@@ -95,36 +95,25 @@ export default function DeviceSettingsPage() {
if (!deviceId) {
return (
<ErrorMessage
message="شناسه دستگاه مشخص نشده است"
fullPage
action={
<BackLink href="/devices" label="بازگشت به انتخاب دستگاه" />
}
/>
<AppShell
pageTitle="تنظیمات دستگاه"
pageIcon={Settings}
iconGradient="from-blue-500 to-blue-600"
>
<ErrorMessage
message="شناسه دستگاه مشخص نشده است"
/>
</AppShell>
)
}
return (
<div className="min-h-screen p-4 md:p-6">
<AppShell
pageTitle={`تنظیمات ${deviceName}`}
pageIcon={Settings}
iconGradient="from-blue-500 to-blue-600"
>
<div className="max-w-5xl mx-auto">
{/* Header */}
<BackLink href="/devices" label="بازگشت به انتخاب دستگاه" />
<PageHeader
icon={Settings}
title={`تنظیمات ${deviceName}`}
iconGradient="from-blue-500 to-blue-600"
action={
<Link
href={`/alert-settings?deviceId=${deviceId}`}
className="flex items-center gap-2 px-4 py-2.5 bg-white border-2 border-orange-300 hover:border-orange-500 hover:bg-orange-50 text-orange-700 hover:text-orange-800 rounded-xl transition-all duration-200 font-medium shadow-sm hover:shadow-md"
>
<Bell className="w-5 h-5" />
<span className="hidden sm:inline">تنظیمات هشدار</span>
</Link>
}
/>
{/* Messages */}
{error && <ErrorMessage message={error} className="mb-6" />}
{success && <SuccessMessage message={success} className="mb-6" />}
@@ -252,7 +241,7 @@ export default function DeviceSettingsPage() {
</Card>
)}
</div>
</div>
</AppShell>
)
}

View File

@@ -5,13 +5,15 @@ import { api, DeviceDto, PagedResult } from '@/lib/api'
import { Settings, LogOut } from 'lucide-react'
import Loading from '@/components/Loading'
import { getCurrentPersianYear, getCurrentPersianMonth, getCurrentPersianDay } from '@/lib/date/persian-date'
import { PageHeader, ErrorMessage, EmptyState, BackLink } from '@/components/common'
import { ErrorMessage, EmptyState } from '@/components/common'
import { SearchInput } from '@/components/forms'
import { DeviceCard } from '@/components/cards'
import { Pagination } from '@/components/navigation'
import { useDevice } from '@/components/layout'
export default function DevicesPage() {
const router = useRouter()
const { setDevice } = useDevice()
const [pagedResult, setPagedResult] = useState<PagedResult<DeviceDto> | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@@ -22,6 +24,12 @@ export default function DevicesPage() {
const pageSize = 10
const handleDeviceSelect = useCallback((device: DeviceDto) => {
setDevice(device.id, device.deviceName)
const today = `${getCurrentPersianYear()}/${String(getCurrentPersianMonth()).padStart(2, '0')}/${String(getCurrentPersianDay()).padStart(2, '0')}`
router.push(`/daily-report?deviceId=${device.id}&date=${encodeURIComponent(today)}`)
}, [setDevice, router])
const fetchDevices = useCallback(async (page: number, search?: string) => {
if (!user) return
@@ -39,9 +47,8 @@ export default function DevicesPage() {
if (result.items.length === 0 && page === 1) {
setError('شما هیچ دستگاهی ندارید. لطفاً با پشتیبانی تماس بگیرید.')
} else if (result.items.length === 1 && result.totalCount === 1 && !search) {
// Single device - redirect to today's daily report
const today = `${getCurrentPersianYear()}/${String(getCurrentPersianMonth()).padStart(2, '0')}/${String(getCurrentPersianDay()).padStart(2, '0')}`
router.push(`/daily-report?deviceId=${result.items[0].id}&date=${encodeURIComponent(today)}`)
// Single device - set context and redirect to today's daily report
handleDeviceSelect(result.items[0])
return
} else {
setPagedResult(result)
@@ -52,7 +59,7 @@ export default function DevicesPage() {
} finally {
setLoading(false)
}
}, [user, router])
}, [user, router, handleDeviceSelect])
useEffect(() => {
// Check authentication
@@ -120,21 +127,34 @@ export default function DevicesPage() {
<div className="min-h-screen p-4 md:p-8">
<div className="max-w-6xl mx-auto">
{/* Header */}
<PageHeader
icon={Settings}
title="انتخاب دستگاه"
subtitle={user ? `${user.name} ${user.family} (${user.mobile})` : undefined}
action={
<button
onClick={handleLogout}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-xl transition-all duration-200 text-sm"
>
<LogOut className="w-4 h-4" />
خروج
</button>
}
iconGradient="from-green-500 to-green-600"
/>
<div className="mb-6">
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-3">
<div className="inline-flex items-center justify-center w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl shadow-md">
<Settings className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-2xl md:text-3xl font-bold text-gray-900">
انتخاب دستگاه
</h1>
{user && (
<p className="text-sm text-gray-500 mt-1">
{user.name} {user.family} ({user.mobile})
</p>
)}
</div>
</div>
<div className="flex-shrink-0">
<button
onClick={handleLogout}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-xl transition-all duration-200 text-sm"
>
<LogOut className="w-4 h-4" />
خروج
</button>
</div>
</div>
</div>
{/* Search */}
<div className="mb-6">
@@ -174,16 +194,13 @@ export default function DevicesPage() {
{pagedResult && pagedResult.items.length > 0 ? (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{pagedResult.items.map((device) => {
const today = `${getCurrentPersianYear()}/${String(getCurrentPersianMonth()).padStart(2, '0')}/${String(getCurrentPersianDay()).padStart(2, '0')}`
return (
<DeviceCard
key={device.id}
device={device}
href={`/daily-report?deviceId=${device.id}&date=${encodeURIComponent(today)}`}
/>
)
})}
{pagedResult.items.map((device) => (
<DeviceCard
key={device.id}
device={device}
onClick={() => handleDeviceSelect(device)}
/>
))}
</div>
{/* Pagination */}
@@ -203,11 +220,6 @@ export default function DevicesPage() {
/>
)
)}
{/* Back to Home */}
<div className="mt-6 text-center">
<BackLink href="/" label="بازگشت به صفحه اصلی" />
</div>
</div>
</div>
)

View File

@@ -156,4 +156,24 @@ button, a, [role="button"] {
);
background-size: 2000px 100%;
animation: shimmer 2s infinite;
}
/* Bottom Navigation Safe Area */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.pb-safe {
padding-bottom: env(safe-area-inset-bottom);
}
.h-safe-bottom {
height: env(safe-area-inset-bottom);
}
}
/* Prevent content from being hidden behind bottom nav */
.has-bottom-nav {
padding-bottom: 5rem;
}
@media (max-width: 768px) {
.has-bottom-nav {
padding-bottom: 4.5rem;
}
}

View File

@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
import './globals.css'
import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration'
import UpdateNotification from '@/components/UpdateNotification'
import { DeviceProvider } from '@/components/layout'
export const metadata: Metadata = {
title: 'GreenHome',
@@ -42,7 +43,9 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
<body className='persian-number'>
<ServiceWorkerRegistration />
<UpdateNotification />
{children}
<DeviceProvider>
{children}
</DeviceProvider>
</body>
</html>
)

View File

@@ -6,6 +6,7 @@ import { Smartphone, ArrowLeft } from 'lucide-react'
import Loading from '@/components/Loading'
import { MobileInput } from '@/components/forms'
import { ErrorMessage } from '@/components/common'
import { getCurrentPersianYear, getCurrentPersianMonth, getCurrentPersianDay } from '@/lib/date/persian-date'
export default function Home() {
const router = useRouter()
@@ -20,9 +21,35 @@ export default function Home() {
if (token && userStr) {
try {
JSON.parse(userStr)
// Redirect to devices check
router.push('/devices')
const user = JSON.parse(userStr)
// Try to use the saved device from localStorage first
const savedDeviceId = localStorage.getItem('selectedDeviceId')
if (savedDeviceId) {
const today = `${getCurrentPersianYear()}/${String(getCurrentPersianMonth()).padStart(2, '0')}/${String(getCurrentPersianDay()).padStart(2, '0')}`
router.push(`/daily-report?deviceId=${savedDeviceId}&date=${encodeURIComponent(today)}`)
return
}
// No saved device — fetch user devices and auto-select the first one
api.getUserDevices(user.id)
.then((devices) => {
if (devices.length > 0) {
const firstDevice = devices[0]
// Save to localStorage
localStorage.setItem('selectedDeviceId', String(firstDevice.id))
localStorage.setItem('selectedDeviceName', firstDevice.deviceName)
const today = `${getCurrentPersianYear()}/${String(getCurrentPersianMonth()).padStart(2, '0')}/${String(getCurrentPersianDay()).padStart(2, '0')}`
router.push(`/daily-report?deviceId=${firstDevice.id}&date=${encodeURIComponent(today)}`)
} else {
// No devices — go to devices page to show the error
router.push('/devices')
}
})
.catch(() => {
// On error, fall back to devices page
router.push('/devices')
})
} catch {
// Invalid user data, go to login
}

138
src/app/profile/page.tsx Normal file
View File

@@ -0,0 +1,138 @@
"use client"
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { User, LogOut, Settings, ChevronLeft, Smartphone, Shield, Monitor } from 'lucide-react'
import { AppShell, useDevice } from '@/components/layout'
import { Card } from '@/components/common'
type UserData = {
id: number
mobile: string
name: string
family: string
role: number
}
export default function ProfilePage() {
const router = useRouter()
const { deviceId } = useDevice()
const [user, setUser] = useState<UserData | null>(null)
useEffect(() => {
const userStr = localStorage.getItem('user')
if (userStr) {
try {
setUser(JSON.parse(userStr))
} catch {
// Invalid user data
}
}
}, [])
const handleLogout = () => {
localStorage.removeItem('authToken')
localStorage.removeItem('user')
router.push('/')
}
const handleDeviceSettings = () => {
router.push(`/device-settings?deviceId=${deviceId}`)
}
const handleSelectDevice = () => {
router.push('/devices')
}
return (
<AppShell
pageTitle="پروفایل"
pageIcon={User}
iconGradient="from-green-500 to-emerald-600"
>
<div className="max-w-2xl mx-auto space-y-6">
{/* User Info Card */}
<Card padding="lg">
<div className="flex items-center gap-4 mb-6">
<div className="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl flex items-center justify-center shadow-md">
<User className="w-8 h-8 text-white" />
</div>
<div>
<h2 className="text-xl font-bold text-gray-900">
{user ? `${user.name} ${user.family}` : 'کاربر'}
</h2>
<p className="text-sm text-gray-500 mt-1 flex items-center gap-1.5">
<Smartphone className="w-4 h-4" />
{user?.mobile || '---'}
</p>
</div>
</div>
<div className="border-t border-gray-100 pt-4 space-y-3">
<div className="flex items-center justify-between py-2">
<span className="text-sm text-gray-600">نقش کاربری</span>
<span className="text-sm font-medium text-gray-900 flex items-center gap-1.5">
<Shield className="w-4 h-4 text-green-600" />
{user?.role === 1 ? 'مدیر' : 'کاربر'}
</span>
</div>
</div>
</Card>
{/* Actions */}
<Card padding="lg">
<div className="space-y-2">
{/* Select Device — always visible so user can change device */}
<button
onClick={handleSelectDevice}
className="w-full flex items-center justify-between p-4 rounded-xl hover:bg-gray-50 transition-colors group"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
<Monitor className="w-5 h-5 text-purple-600" />
</div>
<div className="text-right">
<p className="font-medium text-gray-900">انتخاب دستگاه</p>
<p className="text-xs text-gray-500">تغییر دستگاه فعال</p>
</div>
</div>
<ChevronLeft className="w-5 h-5 text-gray-400 group-hover:-translate-x-1 transition-transform" />
</button>
<button
onClick={handleDeviceSettings}
className="w-full flex items-center justify-between p-4 rounded-xl hover:bg-gray-50 transition-colors group"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
<Settings className="w-5 h-5 text-blue-600" />
</div>
<div className="text-right">
<p className="font-medium text-gray-900">تنظیمات دستگاه</p>
<p className="text-xs text-gray-500">مدیریت محدوده‌های مجاز سنسورها</p>
</div>
</div>
<ChevronLeft className="w-5 h-5 text-gray-400 group-hover:-translate-x-1 transition-transform" />
</button>
<button
onClick={handleLogout}
className="w-full flex items-center justify-between p-4 rounded-xl hover:bg-red-50 transition-colors group"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
<LogOut className="w-5 h-5 text-red-600" />
</div>
<div className="text-right">
<p className="font-medium text-red-600">خروج از حساب</p>
<p className="text-xs text-gray-500">خروج از حساب کاربری</p>
</div>
</div>
<ChevronLeft className="w-5 h-5 text-gray-400 group-hover:-translate-x-1 transition-transform" />
</button>
</div>
</Card>
</div>
</AppShell>
)
}

104
src/app/weather/page.tsx Normal file
View File

@@ -0,0 +1,104 @@
"use client"
import { useState, useEffect, useCallback } from 'react'
import { CloudSun, RefreshCw, MapPin } from 'lucide-react'
import { AppShell, useDevice } from '@/components/layout'
import { WeatherData, fetchForecastWeather, fetchLocationName } from '@/features/weather'
import { QOM_LAT, QOM_LON } from '@/features/weather/helpers'
import { TodayWeather } from '@/components/daily-report/weather/TodayWeather'
import Loading from '@/components/Loading'
import { ErrorMessage } from '@/components/common'
import { Button } from '@/components/common/Button'
export default function WeatherPage() {
const { deviceName } = useDevice()
const [weatherData, setWeatherData] = useState<WeatherData | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [expandedDayIndex, setExpandedDayIndex] = useState<number | null>(null)
const [locationName, setLocationName] = useState<string>('در حال دریافت...')
const loadWeather = useCallback(async () => {
setLoading(true)
setError(null)
try {
const [weather, location] = await Promise.all([
fetchForecastWeather(),
fetchLocationName(QOM_LAT, QOM_LON)
])
setWeatherData(weather)
setLocationName(location)
} catch (error) {
console.error('Error loading weather:', error)
setError('خطا در دریافت اطلاعات آب و هوا. لطفاً دوباره تلاش کنید.')
setLocationName('کهک قم، ایران')
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
loadWeather()
}, [loadWeather])
const handleRefresh = () => {
setWeatherData(null)
loadWeather()
}
return (
<AppShell
pageTitle="آب و هوا"
pageIcon={CloudSun}
pageInfo={deviceName || undefined}
iconGradient="from-sky-500 to-cyan-600"
>
<div className="max-w-4xl mx-auto space-y-6">
{/* Location Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-gray-600">
<MapPin className="w-5 h-5 text-sky-500" />
<span className="font-medium">{locationName}</span>
</div>
<Button
onClick={handleRefresh}
variant="outline"
icon={RefreshCw}
disabled={loading}
>
بروزرسانی
</Button>
</div>
{loading && (
<Loading message="در حال دریافت اطلاعات آب و هوا..." fullScreen={false} />
)}
{error && (
<ErrorMessage
message={error}
action={
<Button
onClick={handleRefresh}
variant="primary"
icon={RefreshCw}
>
تلاش مجدد
</Button>
}
/>
)}
{weatherData && !loading && (
<TodayWeather
weatherData={weatherData}
expandedDayIndex={expandedDayIndex}
onDayToggle={setExpandedDayIndex}
/>
)}
</div>
</AppShell>
)
}