Первый комит - руссификация
This commit is contained in:
parent
78d9777689
commit
39c23eef2f
BIN
lawn_scheduler.db
Normal file
BIN
lawn_scheduler.db
Normal file
Binary file not shown.
9
package-lock.json
generated
9
package-lock.json
generated
@ -2102,9 +2102,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001667",
|
"version": "1.0.30001726",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
|
||||||
"integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==",
|
"integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -2119,7 +2119,8 @@
|
|||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
|
16
package.json
16
package.json
@ -12,14 +12,14 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@libsql/client": "^0.4.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.18.2",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0"
|
||||||
"@libsql/client": "^0.4.0",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"multer": "^1.4.5-lts.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.1",
|
"@eslint/js": "^9.9.1",
|
||||||
@ -27,16 +27,16 @@
|
|||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.18",
|
"autoprefixer": "^10.4.18",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
"eslint": "^9.9.1",
|
"eslint": "^9.9.1",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.11",
|
"eslint-plugin-react-refresh": "^0.4.11",
|
||||||
"globals": "^15.9.0",
|
"globals": "^15.9.0",
|
||||||
|
"nodemon": "^3.0.2",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.3.0",
|
"typescript-eslint": "^8.3.0",
|
||||||
"vite": "^5.4.2",
|
"vite": "^5.4.2"
|
||||||
"concurrently": "^8.2.2",
|
|
||||||
"nodemon": "^3.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Plus, Filter, Calendar, Scissors, AlertTriangle, Square, CheckCircle, Clock, Map } from 'lucide-react';
|
import { Plus, Filter, Calendar, Scissors, AlertTriangle, Square, CheckCircle, Clock, Map } from './Icons';
|
||||||
import { Zone } from '../types/zone';
|
import { Zone } from '../types/zone';
|
||||||
import { api } from '../services/api';
|
import { api } from '../services/api';
|
||||||
import ZoneCard from './ZoneCard';
|
import ZoneCard from './ZoneCard';
|
||||||
@ -128,9 +128,9 @@ const Dashboard: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
|
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
|
||||||
<Scissors className="h-8 w-8 text-green-600" />
|
<Scissors className="h-8 w-8 text-green-600" />
|
||||||
Lawn Care Manager
|
Менеджер по уходу за газоном
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-gray-600">Keep track of your lawn mowing schedule</p>
|
<p className="mt-2 text-gray-600">Следите за своим графиком стрижки газона</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* View Toggle */}
|
{/* View Toggle */}
|
||||||
@ -144,7 +144,7 @@ const Dashboard: React.FC = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Calendar className="h-4 w-4 inline mr-2" />
|
<Calendar className="h-4 w-4 inline mr-2" />
|
||||||
Dashboard
|
Панель
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('sitePlan')}
|
onClick={() => setView('sitePlan')}
|
||||||
@ -155,7 +155,7 @@ const Dashboard: React.FC = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Map className="h-4 w-4 inline mr-2" />
|
<Map className="h-4 w-4 inline mr-2" />
|
||||||
Site Plan
|
План участка
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -164,7 +164,7 @@ const Dashboard: React.FC = () => {
|
|||||||
className="bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg flex items-center gap-2 transition-colors duration-200 shadow-lg hover:shadow-xl"
|
className="bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg flex items-center gap-2 transition-colors duration-200 shadow-lg hover:shadow-xl"
|
||||||
>
|
>
|
||||||
<Plus className="h-5 w-5" />
|
<Plus className="h-5 w-5" />
|
||||||
Add Zone
|
Новая зона
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -183,7 +183,7 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-green-500">
|
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-green-500">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">Total Zones</p>
|
<p className="text-sm font-medium text-gray-600">Количество зон</p>
|
||||||
<p className="text-2xl font-bold text-gray-900">{zones.length}</p>
|
<p className="text-2xl font-bold text-gray-900">{zones.length}</p>
|
||||||
</div>
|
</div>
|
||||||
<Calendar className="h-8 w-8 text-green-500" />
|
<Calendar className="h-8 w-8 text-green-500" />
|
||||||
@ -193,9 +193,9 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-purple-500">
|
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-purple-500">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">Total Area</p>
|
<p className="text-sm font-medium text-gray-600">Общая площадь</p>
|
||||||
<p className="text-2xl font-bold text-gray-900">{totalArea.toLocaleString()}</p>
|
<p className="text-2xl font-bold text-gray-900">{totalArea.toLocaleString()}</p>
|
||||||
<p className="text-xs text-gray-500">sq ft</p>
|
<p className="text-xs text-gray-500">м2</p>
|
||||||
</div>
|
</div>
|
||||||
<Square className="h-8 w-8 text-purple-500" />
|
<Square className="h-8 w-8 text-purple-500" />
|
||||||
</div>
|
</div>
|
||||||
@ -204,9 +204,9 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-emerald-500">
|
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-emerald-500">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">Mowed Area</p>
|
<p className="text-sm font-medium text-gray-600">Скошенная площадь</p>
|
||||||
<p className="text-2xl font-bold text-gray-900">{mowedArea.toLocaleString()}</p>
|
<p className="text-2xl font-bold text-gray-900">{mowedArea.toLocaleString()}</p>
|
||||||
<p className="text-xs text-gray-500">sq ft ({mowedPercentage.toFixed(1)}%)</p>
|
<p className="text-xs text-gray-500">м2 ({mowedPercentage.toFixed(1)}%)</p>
|
||||||
</div>
|
</div>
|
||||||
<CheckCircle className="h-8 w-8 text-emerald-500" />
|
<CheckCircle className="h-8 w-8 text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
@ -215,9 +215,9 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-amber-500">
|
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-amber-500">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">Needs Mowing</p>
|
<p className="text-sm font-medium text-gray-600">Нужно скосить</p>
|
||||||
<p className="text-2xl font-bold text-gray-900">{remainingArea.toLocaleString()}</p>
|
<p className="text-2xl font-bold text-gray-900">{remainingArea.toLocaleString()}</p>
|
||||||
<p className="text-xs text-gray-500">sq ft ({(100 - mowedPercentage).toFixed(1)}%)</p>
|
<p className="text-xs text-gray-500">м2 ({(100 - mowedPercentage).toFixed(1)}%)</p>
|
||||||
</div>
|
</div>
|
||||||
<Clock className="h-8 w-8 text-amber-500" />
|
<Clock className="h-8 w-8 text-amber-500" />
|
||||||
</div>
|
</div>
|
||||||
@ -226,7 +226,7 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-orange-500">
|
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-orange-500">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">Due Today</p>
|
<p className="text-sm font-medium text-gray-600">Срок - сегодня</p>
|
||||||
<p className="text-2xl font-bold text-gray-900">{dueCount}</p>
|
<p className="text-2xl font-bold text-gray-900">{dueCount}</p>
|
||||||
</div>
|
</div>
|
||||||
<Scissors className="h-8 w-8 text-orange-500" />
|
<Scissors className="h-8 w-8 text-orange-500" />
|
||||||
@ -236,7 +236,7 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-red-500">
|
<div className="bg-white rounded-lg shadow-md p-6 border-l-4 border-red-500">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">Overdue</p>
|
<p className="text-sm font-medium text-gray-600">Срок прошел</p>
|
||||||
<p className="text-2xl font-bold text-gray-900">{overdueCount}</p>
|
<p className="text-2xl font-bold text-gray-900">{overdueCount}</p>
|
||||||
</div>
|
</div>
|
||||||
<AlertTriangle className="h-8 w-8 text-red-500" />
|
<AlertTriangle className="h-8 w-8 text-red-500" />
|
||||||
@ -248,9 +248,9 @@ const Dashboard: React.FC = () => {
|
|||||||
{totalArea > 0 && (
|
{totalArea > 0 && (
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Mowing Progress</h3>
|
<h3 className="text-lg font-semibold text-gray-900">Ход скашивания</h3>
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
{mowedArea.toLocaleString()} / {totalArea.toLocaleString()} sq ft
|
{mowedArea.toLocaleString()} / {totalArea.toLocaleString()} м2
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-4 mb-2">
|
<div className="w-full bg-gray-200 rounded-full h-4 mb-2">
|
||||||
@ -262,11 +262,11 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="flex justify-between text-sm text-gray-600">
|
<div className="flex justify-between text-sm text-gray-600">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<CheckCircle className="h-4 w-4 text-emerald-500" />
|
<CheckCircle className="h-4 w-4 text-emerald-500" />
|
||||||
{mowedPercentage.toFixed(1)}% Complete
|
{mowedPercentage.toFixed(1)}% Завершено
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock className="h-4 w-4 text-amber-500" />
|
<Clock className="h-4 w-4 text-amber-500" />
|
||||||
{(100 - mowedPercentage).toFixed(1)}% Remaining
|
{(100 - mowedPercentage).toFixed(1)}% Осталось
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -276,12 +276,12 @@ const Dashboard: React.FC = () => {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Filter className="h-5 w-5 text-gray-500" />
|
<Filter className="h-5 w-5 text-gray-500" />
|
||||||
<span className="text-sm font-medium text-gray-700">Filter:</span>
|
<span className="text-sm font-medium text-gray-700">Фильтр:</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{[
|
{[
|
||||||
{ key: 'all' as FilterType, label: 'All Zones', count: zones.length },
|
{ key: 'all' as FilterType, label: 'Все зоны', count: zones.length },
|
||||||
{ key: 'due' as FilterType, label: 'Due Today', count: dueCount },
|
{ key: 'due' as FilterType, label: 'Срок - сегодня', count: dueCount },
|
||||||
{ key: 'overdue' as FilterType, label: 'Overdue', count: overdueCount },
|
{ key: 'overdue' as FilterType, label: 'Срок прошел', count: overdueCount },
|
||||||
].map(({ key, label, count }) => (
|
].map(({ key, label, count }) => (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
|
117
src/components/Icons.tsx
Normal file
117
src/components/Icons.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// Custom SVG icons to replace Lucide React
|
||||||
|
export const Plus: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Filter: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Calendar: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Scissors: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<circle cx="6" cy="6" r="3" />
|
||||||
|
<circle cx="6" cy="18" r="3" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="m21 12-7-7m7 7-7 7m7-7H8" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AlertTriangle: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Square: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CheckCircle: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Clock: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<polyline points="12,6 12,12 16,14" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Map: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<polygon points="1,6 1,22 8,18 16,22 23,18 23,2 16,6 8,2" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
|
||||||
|
<line x1="8" y1="2" x2="8" y2="18" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
|
||||||
|
<line x1="16" y1="6" x2="16" y2="22" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Edit: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Trash2: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<polyline points="3,6 5,6 21,6" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="m19,6v14a2,2 0 0,1-2,2H7a2,2 0 0,1-2-2V6m3,0V4a2,2 0 0,1,2-2h4a2,2 0 0,1,2,2v2" />
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Camera: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2v11z" />
|
||||||
|
<circle cx="12" cy="13" r="4" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const X: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Upload: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MapPin: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" />
|
||||||
|
<circle cx="12" cy="10" r="3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Eye: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const EyeOff: React.FC<{ className?: string }> = ({ className = "h-6 w-6" }) => (
|
||||||
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24" />
|
||||||
|
<line x1="1" y1="1" x2="23" y2="23" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
|
||||||
|
</svg>
|
||||||
|
);
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { MapPin, Upload, X, Eye, EyeOff } from 'lucide-react';
|
import { MapPin, Upload, X, Eye, EyeOff } from './Icons';
|
||||||
import { Zone } from '../types/zone';
|
import { Zone } from '../types/zone';
|
||||||
|
|
||||||
interface ZoneMarker {
|
interface ZoneMarker {
|
||||||
@ -100,9 +100,9 @@ const SitePlan: React.FC<SitePlanProps> = ({ zones, onZoneSelect, selectedZoneId
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<MapPin className="mx-auto h-16 w-16 text-gray-400" />
|
<MapPin className="mx-auto h-16 w-16 text-gray-400" />
|
||||||
<h3 className="mt-4 text-lg font-semibold text-gray-900">Site Plan</h3>
|
<h3 className="mt-4 text-lg font-semibold text-gray-900">План участка</h3>
|
||||||
<p className="mt-2 text-gray-600">
|
<p className="mt-2 text-gray-600">
|
||||||
Upload an image of your property to visualize zone locations
|
Загрузите изображение участка, чтобы визуализировать расположение зон
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ const SitePlan: React.FC<SitePlanProps> = ({ zones, onZoneSelect, selectedZoneId
|
|||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg flex items-center gap-2 mx-auto transition-colors duration-200"
|
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg flex items-center gap-2 mx-auto transition-colors duration-200"
|
||||||
>
|
>
|
||||||
<Upload className="h-5 w-5" />
|
<Upload className="h-5 w-5" />
|
||||||
Upload Site Plan
|
Загрузка плана участка
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -123,7 +123,7 @@ const SitePlan: React.FC<SitePlanProps> = ({ zones, onZoneSelect, selectedZoneId
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="mt-4 text-sm text-gray-500">
|
<p className="mt-4 text-sm text-gray-500">
|
||||||
Supported formats: JPG, PNG, GIF (max 10MB)
|
Поддерживаемые форматы: JPG, PNG, GIF (максимально 10MB)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -137,7 +137,7 @@ const SitePlan: React.FC<SitePlanProps> = ({ zones, onZoneSelect, selectedZoneId
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<MapPin className="h-6 w-6 text-blue-600" />
|
<MapPin className="h-6 w-6 text-blue-600" />
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Site Plan</h3>
|
<h3 className="text-lg font-semibold text-gray-900">План участка</h3>
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
({zoneMarkers.length} of {zones.length} zones marked)
|
({zoneMarkers.length} of {zones.length} zones marked)
|
||||||
</span>
|
</span>
|
||||||
@ -199,7 +199,7 @@ const SitePlan: React.FC<SitePlanProps> = ({ zones, onZoneSelect, selectedZoneId
|
|||||||
<img
|
<img
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
src={sitePlanImage}
|
src={sitePlanImage}
|
||||||
alt="Site Plan"
|
alt="План участка"
|
||||||
className={`w-full h-auto max-h-[600px] object-contain ${
|
className={`w-full h-auto max-h-[600px] object-contain ${
|
||||||
isEditMode ? 'cursor-crosshair' : 'cursor-default'
|
isEditMode ? 'cursor-crosshair' : 'cursor-default'
|
||||||
}`}
|
}`}
|
||||||
@ -246,7 +246,7 @@ const SitePlan: React.FC<SitePlanProps> = ({ zones, onZoneSelect, selectedZoneId
|
|||||||
<div className="bg-gray-900 text-white text-xs rounded-md px-2 py-1 whitespace-nowrap">
|
<div className="bg-gray-900 text-white text-xs rounded-md px-2 py-1 whitespace-nowrap">
|
||||||
<div className="font-medium">{zone.name}</div>
|
<div className="font-medium">{zone.name}</div>
|
||||||
<div className="text-gray-300">
|
<div className="text-gray-300">
|
||||||
{zone.isOverdue ? 'Overdue' : zone.isDueToday ? 'Due today' : `${zone.daysUntilNext} days`}
|
{zone.isOverdue ? 'Срок прошел' : zone.isDueToday ? 'Срок - сегодня' : `${zone.daysUntilNext} дней`}
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
|
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -280,7 +280,7 @@ const SitePlan: React.FC<SitePlanProps> = ({ zones, onZoneSelect, selectedZoneId
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-3 h-3 rounded-full bg-orange-500 border border-orange-600"></div>
|
<div className="w-3 h-3 rounded-full bg-orange-500 border border-orange-600"></div>
|
||||||
<span className="text-gray-700">Due today</span>
|
<span className="text-gray-700">Срок - сегодня</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-3 h-3 rounded-full bg-red-500 border border-red-600"></div>
|
<div className="w-3 h-3 rounded-full bg-red-500 border border-red-600"></div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Scissors, Edit, Trash2, Calendar, Camera, Square } from 'lucide-react';
|
import { Scissors, Edit, Trash2, Calendar, Camera, Square } from './Icons';
|
||||||
import { Zone } from '../types/zone';
|
import { Zone } from '../types/zone';
|
||||||
|
|
||||||
interface ZoneCardProps {
|
interface ZoneCardProps {
|
||||||
@ -28,11 +28,11 @@ const ZoneCard: React.FC<ZoneCardProps> = ({
|
|||||||
|
|
||||||
const getStatusText = (zone: Zone) => {
|
const getStatusText = (zone: Zone) => {
|
||||||
if (zone.isOverdue) {
|
if (zone.isOverdue) {
|
||||||
return `${Math.abs(zone.daysUntilNext)} days overdue`;
|
return `${Math.abs(zone.daysUntilNext)} дней посроченно`;
|
||||||
} else if (zone.isDueToday) {
|
} else if (zone.isDueToday) {
|
||||||
return 'Due today';
|
return 'Срок - сегодня';
|
||||||
} else {
|
} else {
|
||||||
return `${zone.daysUntilNext} days remaining`;
|
return `${zone.daysUntilNext} дней осталось`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ const ZoneCard: React.FC<ZoneCardProps> = ({
|
|||||||
|
|
||||||
const formatArea = (area: number) => {
|
const formatArea = (area: number) => {
|
||||||
if (area === 0) return 'Not specified';
|
if (area === 0) return 'Not specified';
|
||||||
return `${area.toLocaleString()} sq ft`;
|
return `${area.toLocaleString()} м2`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -94,15 +94,15 @@ const ZoneCard: React.FC<ZoneCardProps> = ({
|
|||||||
<div className="space-y-2 mb-4 text-sm text-gray-600">
|
<div className="space-y-2 mb-4 text-sm text-gray-600">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
<span>Last mowed: {formatDate(zone.lastMowedDate)}</span>
|
<span>Было скошенно: {formatDate(zone.lastMowedDate)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Scissors className="h-4 w-4" />
|
<Scissors className="h-4 w-4" />
|
||||||
<span>Every {zone.intervalDays} days</span>
|
<span>Каждые {zone.intervalDays} дней</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Square className="h-4 w-4" />
|
<Square className="h-4 w-4" />
|
||||||
<span>Area: {formatArea(zone.area)}</span>
|
<span>Площадь: {formatArea(zone.area)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ const ZoneCard: React.FC<ZoneCardProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Scissors className="h-4 w-4 inline mr-1" />
|
<Scissors className="h-4 w-4 inline mr-1" />
|
||||||
Mowed
|
Скошенно
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { X, Upload, Camera } from 'lucide-react';
|
import { X, Upload, Camera } from './Icons';
|
||||||
import { Zone, ZoneFormData } from '../types/zone';
|
import { Zone, ZoneFormData } from '../types/zone';
|
||||||
import { api } from '../services/api';
|
import { api } from '../services/api';
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ const ZoneForm: React.FC<ZoneFormProps> = ({ zone, onSubmit, onCancel }) => {
|
|||||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||||
<div className="flex items-center justify-between p-6 border-b">
|
<div className="flex items-center justify-between p-6 border-b">
|
||||||
<h2 className="text-xl font-semibold text-gray-900">
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
{zone ? 'Edit Zone' : 'Add New Zone'}
|
{zone ? 'Редактирование Зоны' : 'Добавление новой Зоны'}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
@ -80,7 +80,7 @@ const ZoneForm: React.FC<ZoneFormProps> = ({ zone, onSubmit, onCancel }) => {
|
|||||||
{/* Zone Name */}
|
{/* Zone Name */}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Zone Name
|
Наименование зоны
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -89,14 +89,14 @@ const ZoneForm: React.FC<ZoneFormProps> = ({ zone, onSubmit, onCancel }) => {
|
|||||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
placeholder="e.g., Front Yard, Back Garden"
|
placeholder="например Задний Двор, Входная Зона"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Area */}
|
{/* Площадь */}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="area" className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="area" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Area (square feet)
|
Площадь (квадратные метры)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@ -108,13 +108,13 @@ const ZoneForm: React.FC<ZoneFormProps> = ({ zone, onSubmit, onCancel }) => {
|
|||||||
step="0.1"
|
step="0.1"
|
||||||
placeholder="e.g., 150.5"
|
placeholder="e.g., 150.5"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">Optional - helps track total lawn area</p>
|
<p className="text-xs text-gray-500 mt-1">Дополнительно - помогает отслеживать общую площадь газона</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mowing Interval */}
|
{/* Mowing Interval */}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="intervalDays" className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="intervalDays" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Mowing Interval (days)
|
Интервал покоса (дни)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@ -131,7 +131,7 @@ const ZoneForm: React.FC<ZoneFormProps> = ({ zone, onSubmit, onCancel }) => {
|
|||||||
{/* Last Mowed Date */}
|
{/* Last Mowed Date */}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="lastMowedDate" className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="lastMowedDate" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Last Mowed Date
|
Дата последнего покоса
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
@ -146,7 +146,7 @@ const ZoneForm: React.FC<ZoneFormProps> = ({ zone, onSubmit, onCancel }) => {
|
|||||||
{/* Zone Image */}
|
{/* Zone Image */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Zone Image (optional)
|
Изображение зоны (не обязательно)
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{imagePreview ? (
|
{imagePreview ? (
|
||||||
@ -176,8 +176,8 @@ const ZoneForm: React.FC<ZoneFormProps> = ({ zone, onSubmit, onCancel }) => {
|
|||||||
className="border-2 border-dashed border-gray-300 rounded-md p-6 text-center cursor-pointer hover:border-green-500 hover:bg-green-50 transition-colors duration-200"
|
className="border-2 border-dashed border-gray-300 rounded-md p-6 text-center cursor-pointer hover:border-green-500 hover:bg-green-50 transition-colors duration-200"
|
||||||
>
|
>
|
||||||
<Upload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
|
<Upload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
|
||||||
<p className="text-sm text-gray-600">Click to upload zone image</p>
|
<p className="text-sm text-gray-600">Нажмите для загрузки изображения зоны</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">PNG, JPG up to 5MB</p>
|
<p className="text-xs text-gray-500 mt-1">PNG, JPG до 5MB</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -197,14 +197,14 @@ const ZoneForm: React.FC<ZoneFormProps> = ({ zone, onSubmit, onCancel }) => {
|
|||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="flex-1 py-2 px-4 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors duration-200"
|
className="flex-1 py-2 px-4 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors duration-200"
|
||||||
>
|
>
|
||||||
Cancel
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex-1 py-2 px-4 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
className="flex-1 py-2 px-4 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
|
||||||
>
|
>
|
||||||
{loading ? 'Saving...' : zone ? 'Update Zone' : 'Create Zone'}
|
{loading ? 'Сохраняется...' : zone ? 'Изменение Зоны' : 'Создание Зоны'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user