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
18 changes: 18 additions & 0 deletions src/farmtech/client/js/apps/farmtech-change-password.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createRoot } from 'react-dom/client';
import { I18nextProvider } from 'react-i18next';
import { ChangePasswordApp } from '../components/change-password/ChangePasswordApp';
import i18n from '../i18n/config';
import '../styles/react-apps.css';

document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('farmtech-change-password-container');

if (container) {
const root = createRoot(container);
root.render(
<I18nextProvider i18n={i18n}>
<ChangePasswordApp />
</I18nextProvider>
);
}
});
28 changes: 28 additions & 0 deletions src/farmtech/client/js/apps/farmtech-login.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import { I18nextProvider } from 'react-i18next';
import { LoginApp } from '../components/login/loginApp';
import i18n from '../i18n/config';
import '../styles/react-apps.css';

const rootReducer = combineReducers({
login: (state = {}, action) => state
});

const store = createStore(rootReducer);

document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('farmtech-login-container');

if (container) {
const root = createRoot(container);
root.render(
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<LoginApp />
</I18nextProvider>
</Provider>
);
}
});
28 changes: 28 additions & 0 deletions src/farmtech/client/js/apps/farmtech-register.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import { I18nextProvider } from 'react-i18next';
import { RegisterApp } from '../components/register/registerApp';
import i18n from '../i18n/config';
import '../styles/react-apps.css';

const rootReducer = combineReducers({
register: (state = {}, action) => state
});

const store = createStore(rootReducer);

document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('farmtech-register-container');

if (container) {
const root = createRoot(container);
root.render(
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<RegisterApp />
</I18nextProvider>
</Provider>
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useState } from 'react';
import { ChangePasswordButton } from './ChangePasswordButton';
import { ChangePasswordDialog } from './ChangePasswordDialog';

export const ChangePasswordApp = () => {
const [showDialog, setShowDialog] = useState(false);

return (
<>
<ChangePasswordButton onClick={() => setShowDialog(true)} />
{showDialog && (
<ChangePasswordDialog
onClose={() => setShowDialog(false)}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Lock } from 'lucide-react';
import { useTranslation } from 'react-i18next';

export const ChangePasswordButton = ({ onClick }) => {
const { t } = useTranslation('changePassword');

return (
<a
href='#'
onClick={onClick}
>
<Lock size={14} className='me-2'/>
{t('change_password')}
</a>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { useState } from 'react';
import { AlertCircle, CheckCircle, Loader, Eye, EyeOff } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import api from '../../utils/api';

export const ChangePasswordDialog = ({ onClose }) => {
const { t } = useTranslation('changePassword');
const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showOld, setShowOld] = useState(false);
const [showNew, setShowNew] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [message, setMessage] = useState({ text: '', type: '' });

const canSubmit = () => {
return oldPassword && newPassword && confirmPassword && newPassword === confirmPassword;
};

const handleSubmit = async () => {
if (!canSubmit()) return;

if (newPassword !== confirmPassword) {
setMessage({ text: t('passwords_mismatch'), type: 'danger' });
return;
}

setSubmitting(true);
setMessage({ text: t('submitting'), type: 'info' });

try {
await api.post('/api/auth/change-password/', {
old_password: oldPassword,
new_password: newPassword,
new_password_confirm: confirmPassword,
});

setMessage({ text: t('password_changed'), type: 'success' });
setTimeout(() => {
window.location.href = '/account/login';
}, 2000);
} catch (error) {
const errorData = error.data;
let errorMsg = t('error_generic');

if (errorData) {
if (errorData.old_password) {
errorMsg = t('error_old_password');
} else if (errorData.new_password) {
errorMsg = Array.isArray(errorData.new_password)
? errorData.new_password.join(' ')
: errorData.new_password;
} else if (errorData.detail) {
errorMsg = errorData.detail;
}
}

setMessage({ text: errorMsg, type: 'danger' });
} finally {
setSubmitting(false);
}
};

const passwordsMatch = !confirmPassword || newPassword === confirmPassword;

return (
<div
className="modal show d-block"
tabIndex="-1"
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
onClick={(e) => {
if (e.target.classList.contains('modal')) onClose();
}}
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">{t('change_password')}</h5>
<button
type="button"
className="btn-close"
onClick={onClose}
aria-label="Close"
></button>
</div>

<div className="modal-body">
{/* Old Password */}
<div className="mb-3">
<label htmlFor="old-password" className="form-label">
{t('old_password')}
</label>
<div className="input-group">
<input
id="old-password"
type={showOld ? 'text' : 'password'}
className="form-control"
value={oldPassword}
onChange={(e) => {
setOldPassword(e.target.value);
setMessage({ text: '', type: '' });
}}
disabled={submitting}
/>
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => setShowOld(!showOld)}
tabIndex={-1}
>
{showOld ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>

{/* New Password */}
<div className="mb-3">
<label htmlFor="new-password" className="form-label">
{t('new_password')}
</label>
<div className="input-group">
<input
id="new-password"
type={showNew ? 'text' : 'password'}
className="form-control"
value={newPassword}
onChange={(e) => {
setNewPassword(e.target.value);
setMessage({ text: '', type: '' });
}}
disabled={submitting}
/>
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => setShowNew(!showNew)}
tabIndex={-1}
>
{showNew ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>

{/* Confirm New Password */}
<div className="mb-3">
<label htmlFor="confirm-password" className="form-label">
{t('confirm_password')}
</label>
<div className="input-group">
<input
id="confirm-password"
type={showConfirm ? 'text' : 'password'}
className={`form-control ${!passwordsMatch ? 'is-invalid' : ''}`}
value={confirmPassword}
onChange={(e) => {
setConfirmPassword(e.target.value);
setMessage({ text: '', type: '' });
}}
disabled={submitting}
/>
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => setShowConfirm(!showConfirm)}
tabIndex={-1}
>
{showConfirm ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
{!passwordsMatch && (
<div className="invalid-feedback d-block">
{t('passwords_mismatch')}
</div>
)}
</div>

{/* Message Display */}
{message.text && (
<div className={`alert alert-${message.type} d-flex align-items-center`}>
{message.type === 'success' && (
<CheckCircle size={20} className="me-2 flex-shrink-0" />
)}
{message.type === 'danger' && (
<AlertCircle size={20} className="me-2 flex-shrink-0" />
)}
{message.type === 'info' && (
<Loader size={20} className="me-2 flex-shrink-0 upload-status-loading" />
)}
<div>{message.text}</div>
</div>
)}
</div>

<div className="modal-footer">
<button
type="button"
className="btn btn-secondary text-white"
onClick={onClose}
disabled={submitting}
>
{t('cancel')}
</button>
<button
type="button"
className="btn btn-primary"
onClick={handleSubmit}
disabled={!canSubmit() || submitting}
>
{submitting ? (
<>
<Loader size={16} className="me-2 upload-status-loading" />
{t('submitting')}
</>
) : (
t('submit')
)}
</button>
</div>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export const MapControlButtons = ({
onToggleDrawing,
onAnalyze,
onRemovePolygon,
onToggleTutorial
onToggleTutorial,
isTutorialExpanded
}) => {
const { t } = useTranslation('inference');
const fileInputRef = useRef(null);
Expand Down Expand Up @@ -132,7 +133,7 @@ export const MapControlButtons = ({

{/* Help/Tutorial Button */}
<button
className="btn btn-light shadow-sm d-flex align-items-center justify-content-center rounded-circle"
className={`btn ${isTutorialExpanded ? 'btn-primary' : 'btn-light'} shadow-sm d-flex align-items-center justify-content-center rounded-circle`}
style={{ width: '56px', height: '56px' }}
onClick={onToggleTutorial}
title={t('help', 'Help')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const MapControls = ({
onAnalyze={onAnalyze}
onRemovePolygon={onRemovePolygon}
onToggleTutorial={onToggleExpanded}
isTutorialExpanded={isExpanded}
/>
</div>

Expand Down
Loading