fix ui and add pwa
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 5s

This commit is contained in:
2025-11-15 17:27:10 +03:30
parent 38bc2b4ac2
commit 3870d66f7d
37 changed files with 1777 additions and 811 deletions

View File

@@ -2,6 +2,7 @@
import { useEffect, useMemo, useState } from 'react'
import { api } from '@/lib/api'
import { getCurrentPersianYear, getCurrentPersianMonth } from '@/lib/persian-date'
import Loading from '@/components/Loading'
function useQueryParam(name: string) {
if (typeof window === 'undefined') return null as string | null
@@ -14,11 +15,14 @@ export default function MonthPage() {
const month = Number(useQueryParam('month') ?? getCurrentPersianMonth().toString())
const [daysWithCounts, setDaysWithCounts] = useState<{ day: number; count: number }[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
api.monthDays(deviceId, year, month)
.then(list => setDaysWithCounts(list.map(x => ({ day: Number(x.persianDate.split('/')[2]), count: x.count }))))
.catch(console.error)
.finally(() => setLoading(false))
}, [deviceId, year, month])
const maxDay = useMemo(() => {
@@ -32,6 +36,10 @@ export default function MonthPage() {
const days = useMemo(() => Array.from({ length: maxDay }, (_, i) => i + 1), [maxDay])
const countByDay = useMemo(() => new Map(daysWithCounts.map(d => [d.day, d.count])), [daysWithCounts])
if (loading) {
return <Loading message="در حال بارگذاری تقویم ماه..." />
}
return (
<div className="p-6">
<div className="flex items-center justify-between mb-3">
@@ -51,7 +59,7 @@ export default function MonthPage() {
const c = countByDay.get(d) ?? 0
const hasData = c > 0
return (
<a key={d} href={`/day_details?deviceId=${deviceId}&year=${year}&month=${month}&day=${d}`} className={`min-h-[84px] bg-white p-2 flex flex-col border-l-0 ${hasData ? 'bg-green-50 hover:bg-green-100' : ''}`}>
<a key={d} href={`/day-details?deviceId=${deviceId}&year=${year}&month=${month}&day=${d}`} className={`min-h-[84px] bg-white p-2 flex flex-col border-l-0 ${hasData ? 'bg-green-50 hover:bg-green-100' : ''}`}>
<div className="text-xs text-gray-500 mb-1">{d}</div>
{hasData && (
<span className="self-start text-[10px] bg-green-600 text-white rounded-full px-1.5 py-0.5">{c}</span>

View File

@@ -2,6 +2,9 @@
import { useEffect, useMemo, useState } from 'react'
import { api } from '@/lib/api'
import { getCurrentPersianYear } from '@/lib/persian-date'
import { Calendar as CalendarIcon, ChevronRight, Database, TrendingUp } from 'lucide-react'
import Link from 'next/link'
import Loading from '@/components/Loading'
const monthNames = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
@@ -24,6 +27,7 @@ export default function CalendarPage() {
const [year, setYear] = useState<number>(getCurrentPersianYear())
const [activeMonths, setActiveMonths] = useState<number[]>([])
const [monthDays, setMonthDays] = useState<Record<number, { days: number; records: number }>>({})
const [loading, setLoading] = useState(true)
const years = useMemo(() => Array.from({ length: 10 }, (_, i) => getCurrentPersianYear() - 2 + i), [])
// وقتی deviceIdParam تغییر کرد، deviceId را به روز کن
@@ -37,6 +41,7 @@ export default function CalendarPage() {
let mounted = true
setMonthDays({})
setActiveMonths([])
setLoading(true)
api.activeMonths(deviceId, year)
.then(async (months) => {
@@ -51,44 +56,113 @@ export default function CalendarPage() {
setMonthDays(Object.fromEntries(entries))
})
.catch(console.error)
.finally(() => {
if (mounted) setLoading(false)
})
return () => { mounted = false }
}, [deviceId, year])
const totalDays = useMemo(() => Object.values(monthDays).reduce((s, v) => s + (v?.days ?? 0), 0), [monthDays])
const totalRecords = useMemo(() => Object.values(monthDays).reduce((s, v) => s + (v?.records ?? 0), 0), [monthDays])
if (loading) {
return <Loading message="در حال بارگذاری تقویم..." />
}
return (
<div className="p-6">
<div className="rounded-2xl border bg-white shadow-sm p-6">
<div className="text-center text-xl font-semibold mb-4">انتخاب سال و ماه 📅</div>
<div className="flex flex-wrap items-center justify-center gap-3 mb-4">
<label className="text-sm text-gray-600">انتخاب سال:</label>
<select className="border rounded-md px-2 py-1" value={year} onChange={e => setYear(Number(e.target.value))}>
{years.map(y => <option key={y} value={y}>{y}</option>)}
</select>
<div className="min-h-screen p-4 md:p-6">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-6">
<Link
href="/"
className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 transition-colors mb-4"
>
<ChevronRight className="w-4 h-4" />
بازگشت به صفحه اصلی
</Link>
<div className="flex items-center gap-3 mb-2">
<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">
<CalendarIcon className="w-6 h-6 text-white" />
</div>
<h1 className="text-2xl md:text-3xl font-bold text-gray-900">انتخاب سال و ماه</h1>
</div>
</div>
<div className="text-center text-sm text-gray-600 mb-5">
خلاصه {year}: {totalDays} روز دارای دیتا · {totalRecords} رکورد
</div>
{/* Main Card */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-6 md:p-8">
{/* Year Selector */}
<div className="flex flex-wrap items-center justify-center gap-4 mb-6">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<CalendarIcon className="w-4 h-4 text-gray-500" />
انتخاب سال:
</label>
<select
className="px-4 py-2 border-2 border-gray-200 rounded-xl focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/20 transition-all bg-white font-medium"
value={year}
onChange={e => setYear(Number(e.target.value))}
>
{years.map(y => <option key={y} value={y}>{y}</option>)}
</select>
</div>
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-4">
{monthNames.map((name, idx) => {
const m = idx + 1
const isActive = activeMonths.includes(m)
const stats = monthDays[m]
return (
<a key={m} href={`/day_details?deviceId=${deviceId}&year=${year}&month=${m}`} className={`rounded-xl border p-4 text-center transition ${isActive ? 'bg-white hover:bg-gray-50' : 'opacity-50'}`}>
<div className="text-sm">{name}</div>
{isActive && stats && (
<div className="inline-block mt-3 text-xs bg-green-600 text-white rounded-full px-2 py-1">
{stats.days} روز · {stats.records} رکورد
{/* Summary Stats */}
<div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-xl p-4 mb-6">
<div className="flex items-center justify-center gap-6 flex-wrap">
<div className="flex items-center gap-2">
<Database className="w-5 h-5 text-green-600" />
<span className="text-sm text-gray-700">
<span className="font-semibold text-green-700">{totalDays}</span> روز دارای داده
</span>
</div>
<div className="flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-green-600" />
<span className="text-sm text-gray-700">
<span className="font-semibold text-green-700">{totalRecords}</span> رکورد
</span>
</div>
</div>
</div>
{/* Months Grid */}
<div className="grid gap-4 md:grid-cols-3 xl:grid-cols-4">
{monthNames.map((name, idx) => {
const m = idx + 1
const isActive = activeMonths.includes(m)
const stats = monthDays[m]
return (
<Link
key={m}
href={`/day-details?deviceId=${deviceId}&year=${year}&month=${m}`}
className={`group relative rounded-xl border-2 p-5 text-center transition-all duration-300 ${
isActive
? 'bg-white border-green-200 hover:border-green-400 hover:shadow-lg hover:-translate-y-1'
: 'bg-gray-50 border-gray-200 opacity-50 cursor-not-allowed'
}`}
>
<div className={`text-lg font-semibold mb-2 ${isActive ? 'text-gray-900' : 'text-gray-400'}`}>
{name}
</div>
)}
</a>
)
})}
{isActive && stats ? (
<div className="space-y-1">
<div className="inline-flex items-center gap-1 bg-green-600 text-white text-xs rounded-full px-3 py-1.5 font-medium">
<Database className="w-3 h-3" />
{stats.days} روز
</div>
<div className="text-xs text-gray-600 mt-2">
{stats.records} رکورد
</div>
</div>
) : (
<div className="text-xs text-gray-400">بدون داده</div>
)}
{isActive && (
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-green-500 to-green-600 rounded-b-xl transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300" />
)}
</Link>
)
})}
</div>
</div>
</div>
</div>