Skip to content
Open
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
29 changes: 29 additions & 0 deletions frontend-ui/DEPENDENCY_CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Frontend UI - Dependency Version Changes

## Lockfile Updates (package-lock.json)

This PR includes the following dependency version upgrades in addition to adding Leaflet for map functionality:

### Production Dependencies
- **axios**: `1.13.6` → `1.14.0` (minor version bump, backward compatible)
- **leaflet**: `^1.9.4` (newly added for map functionality)

### Development Dependencies
- **vite**: `8.0.0` → `8.0.3` (patch version bump, bug fixes)
- Various Babel and build tool dependencies updated to compatible versions

## Rationale

These version upgrades are the result of:
1. **Leaflet addition**: A new dependency required for the land parcel map visualization feature
2. **npm install resolution**: When adding leaflet, npm resolved dependencies to the latest patch/minor versions compatible with the `^` constraints in package.json

## Compatibility Notes

- **axios 1.14.0**: Includes minor improvements and is fully backward-compatible with existing API calls
- **vite 8.0.3**: Patch release with bug fixes; no breaking changes
- All upgrades maintain the caret (`^`) version constraints specified in package.json

## Recommendation

If stricter version pinning is desired for production stability, consider updating package.json to use exact versions (e.g., `"vite": "8.0.3"` instead of `"vite": "^8.0.0"`).
314 changes: 180 additions & 134 deletions frontend-ui/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion frontend-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"axios": "^1.13.6",
"leaflet": "^1.9.4",
"react": "^19.2.4",
"react-dom": "^19.2.4"
"react-dom": "^19.2.4",
"react-router-dom": "^6.30.3"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
Expand Down
88 changes: 14 additions & 74 deletions frontend-ui/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,80 +1,20 @@
import { useState } from 'react';
import RegisterLand from './components/RegisterLand';
import SearchLand from './components/SearchLand';
import OwnerSearch from './components/OwnerSearch';
import TransferLand from './components/TransferLand';
import MutateLand from './components/MutateLand';
import AssetHistory from './components/AssetHistory';
import MapView from "./components/Mapview";
import { Routes, Route, Navigate } from 'react-router-dom';
import DashboardLayout from './layouts/DashboardLayout';
import UserLogin from './pages/UserLogin';
import AdminLogin from './pages/AdminLogin';
import ProtectedRoute from './components/auth/ProtectedRoute';
import './index.css';

const NAV_ITEMS = [
{ id: 'register', label: 'Register Parcel', icon: '⊕' },
{ id: 'search', label: 'Search by ULPIN', icon: '◎' },
{ id: 'owner', label: 'Search by Owner', icon: '◈' },
{ id: 'map', label: 'Search by Map', icon: '⊞' },
{ id: 'transfer', label: 'Transfer Ownership', icon: '⇌' },
{ id: 'mutate', label: 'Mutate / Split', icon: '⊗' },
{ id: 'history', label: 'Audit Trail', icon: '◷' },
];

export default function App() {
const [active, setActive] = useState('register');
const [prefill, setPrefill] = useState(null);

const navigateTo = (page, data = null) => {
setPrefill(data);
setActive(page);
};

const renderPage = () => {
switch (active) {
case 'register': return <RegisterLand prefill={prefill} onPrefillUsed={() => setPrefill(null)} />;
case 'search': return <SearchLand />;
case 'owner': return <OwnerSearch />;
case 'map': return <MapView onNavigateRegister={(data) => navigateTo('register', data)} />;
case 'transfer': return <TransferLand />;
case 'mutate': return <MutateLand />;
case 'history': return <AssetHistory />;
default: return null;
}
};

return (
<div className="app-layout">
<aside className="sidebar">
<div className="tricolor-bar" />
<div className="sidebar-logo">
<div className="logo-emblem">⊛</div>
<div className="logo-name">BHUMI REGISTRY</div>
<div className="logo-dept">Ministry of Rural Development</div>
</div>

<nav className="sidebar-nav">
<div className="nav-section-label">Operations</div>
{NAV_ITEMS.map(({ id, label, icon }) => (
<div
key={id}
className={`nav-item${active === id ? ' active' : ''}`}
onClick={() => navigateTo(id)}
>
<span className="nav-icon">{icon}</span>
<span>{label}</span>
</div>
))}
</nav>

<div className="sidebar-footer">
<div className="network-status">
<div className="status-dot" />
<span>Fabric · landchannel</span>
</div>
</div>
</aside>

<main className="main-content" key={active}>
{renderPage()}
</main>
</div>
<Routes>
<Route path="/login" element={<UserLogin />} />
<Route path="/admin/login" element={<AdminLogin />} />
<Route path="/*" element={
<ProtectedRoute>
<DashboardLayout />
</ProtectedRoute>
} />
</Routes>
);
}
45 changes: 36 additions & 9 deletions frontend-ui/src/components/Mapview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,14 @@ async function reverseGeocode(lat, lng, signal) {
if (!res.ok) throw new Error(`Geocoding failed (${res.status})`);
const data = await res.json();
const addr = data.address || {};
// Use country_code (ISO-3166) for stable India detection; normalize case and trim
const countryCode = (addr.country_code || '').trim().toLowerCase();
return {
state: addr.state || '',
district: addr.county || addr.state_district || '',
city: addr.city || addr.town || addr.village || '',
country: addr.country || '',
countryCode, // ISO-3166 code (e.g., 'in' for India)
displayName: data.display_name || '',
};
}
Expand All @@ -82,7 +86,7 @@ export default function MapView({ onNavigateRegister }) {
const [ulpin, setUlpin] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const [isIndia, setIsIndia] = useState(false);
const pinIcon = useCallback(() => L.divIcon({
className: '',
html: `<div style="width:20px;height:20px;background:#0f2d5a;border:3px solid #fff;
Expand Down Expand Up @@ -123,7 +127,15 @@ export default function MapView({ onNavigateRegister }) {
// Discard if a newer click already superseded this one
if (thisReq !== reqIdRef.current) return;
setGeocode(geo);
setUlpin(generateUlpin(geo, lat, lng));
// Use ISO-3166 country code ('in' for India) for stable, localization-independent detection
const isIndiaLocation = geo.countryCode === 'in';
if (isIndiaLocation) {
setIsIndia(true);
setUlpin(generateUlpin(geo, lat, lng));
} else {
setIsIndia(false);
setUlpin('');
}
} catch (err) {
if (err.name === 'AbortError') return; // superseded — silently ignore
if (thisReq !== reqIdRef.current) return;
Expand Down Expand Up @@ -250,12 +262,13 @@ export default function MapView({ onNavigateRegister }) {
)}
</div>

{ulpin && !loading && (
{!loading && geocode && isIndia && (
<div className="card" style={{ padding: '16px 18px' }}>
<div className="card-title" style={{ marginBottom: 13 }}>
<span>⊞</span> Generated ULPIN
</div>
<div className="card-title" style={{ marginBottom: 13 }}>
<span>⊞</span> Generated ULPIN
</div>

<>
<div style={{
background: 'var(--navy-pale)',
border: '1.5px solid var(--border-active)',
Expand All @@ -273,14 +286,28 @@ export default function MapView({ onNavigateRegister }) {
{ulpin}
</span>
</div>

<p style={{ fontSize: 11.5, color: 'var(--text-muted)', lineHeight: 1.6, marginBottom: 14 }}>
<p style={{ fontSize: 11.5, color: 'var(--text-muted)', marginBottom: 14 }}>
Tap below to open the Register form with ULPIN and GPS pre-filled.
</p>

<button className="btn btn-primary" style={{ width: '100%' }} onClick={handleFillRegister}>
Fill Register Form
</button>
</>
</div>
)}

{!loading && geocode && !isIndia && (
<div className="card" style={{ padding: '16px 18px' }}>
<div className="card-title" style={{ marginBottom: 13 }}>
<span>⊞</span> Location Status
</div>

<p style={{ fontSize: 12, color: 'var(--text-muted)' }}>
<strong>ULPIN generation is only available for locations within India.</strong><br/>
Please select a location on the Indian map to enable ULPIN creation.
</p>
</div>
)}
</>
Expand Down
132 changes: 132 additions & 0 deletions frontend-ui/src/components/auth/LoginView.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

export default function LoginView({ title, subtitle, idPlacholder, idLabel, isUserLogin = true }) {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState({
id: '',
password: ''
});

const handleChange = (e) => {
setFormData(prev => ({
...prev,
[e.target.name]: e.target.value
}));
if (error) setError('');
};

const handleSubmit = (e) => {
e.preventDefault();
setLoading(true);
setError('');

// Basic frontend validation
if (!formData.id || !formData.password) {
setLoading(false);
setError('Please fill in all fields.');
return;
}

if (formData.password.length < 5) {
setLoading(false);
setError('Invalid credentials.');
return;
}

// Mock authentication
setTimeout(() => {
setLoading(false);
// On success, set auth state and redirect to dashboard
localStorage.setItem('isAuthenticated', 'true');
localStorage.setItem('userRole', isUserLogin ? 'user' : 'admin');
navigate('/');
}, 1000);
};

return (
<div className="auth-layout">
<div className="auth-container">
<div className="auth-brand">
<div className="auth-logo-emblem">⊛</div>
<div>
<h1 className="auth-logo-name">BHUMI REGISTRY</h1>
<p className="auth-logo-dept">Ministry of Rural Development</p>
</div>
</div>

<div className="card auth-card">
<div className="auth-header">
<h2 className="page-title">{title}</h2>
<p className="page-subtitle">{subtitle}</p>
</div>

{error && (
<div className="alert alert-error" style={{ marginBottom: '20px' }}>
<span>{error}</span>
</div>
)}

<form className="form-grid" onSubmit={handleSubmit}>
<div className="form-field">
<label className="field-label">{idLabel || (isUserLogin ? 'Email / Username' : 'Admin ID / Email')}</label>
<input
type="text"
name="id"
value={formData.id}
onChange={handleChange}
className="field-input"
placeholder={idPlacholder}
disabled={loading}
/>
</div>

<div className="form-field">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<label className="field-label">Password</label>
{isUserLogin && (
<a href="#" className="auth-link" onClick={(e) => e.preventDefault()}>Forgot password?</a>
)}
</div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
className="field-input"
placeholder="Enter your password"
disabled={loading}
/>
</div>

{isUserLogin && (
<div className="form-field" style={{ flexDirection: 'row', alignItems: 'center', gap: '8px', marginTop: '4px' }}>
<input type="checkbox" id="rememberMe" />
<label htmlFor="rememberMe" className="field-hint" style={{ fontSize: '13px' }}>Remember me</label>
</div>
)}

<button type="submit" className="btn btn-primary" style={{ marginTop: '10px', width: '100%' }} disabled={loading}>
{loading ? (
<>
<div className="spinner" style={{ width: '15px', height: '15px', borderWidth: '2px' }}></div>
Authenticating...
</>
) : 'Sign In'}
</button>
</form>

<div className="auth-footer">
{isUserLogin ? (
<p className="field-hint">Are you an administrator? <span className="auth-link" onClick={() => navigate('/admin/login')}>Admin Login</span></p>
) : (
<p className="field-hint">Are you a citizen? <span className="auth-link" onClick={() => navigate('/login')}>User Login</span></p>
)}
</div>
</div>
</div>
</div>
);
}
11 changes: 11 additions & 0 deletions frontend-ui/src/components/auth/ProtectedRoute.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Navigate } from 'react-router-dom';

export default function ProtectedRoute({ children }) {
const isAuthenticated = localStorage.getItem('isAuthenticated') === 'true';

if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}

return children;
}
Loading