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
10 changes: 0 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
<<<<<<< HEAD
=======
"dompurify": "^3.2.4",
>>>>>>> bc54ed8 (clean: remove node_modules and reset repo)
"framer-motion": "^12.23.0",
"idb": "^8.0.0",
"lucide-react": "^0.462.0",
Expand All @@ -57,20 +54,13 @@
"react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0",
"react-intersection-observer": "^10.0.3",
<<<<<<< HEAD
=======
"react-virtualized-auto-sizer": "^1.0.7",
"react-window": "^1.8.9",
>>>>>>> bc54ed8 (clean: remove node_modules and reset repo)
"recharts": "^2.15.4",
"socket.io-client": "^4.8.3",
"tailwind-merge": "^2.6.0",
"web-vitals": "^4.2.4",
"workbox-webpack-plugin": "^7.0.0",
<<<<<<< HEAD
"dompurify": "^3.2.4",
=======
>>>>>>> bc54ed8 (clean: remove node_modules and reset repo)
"zod": "^3.25.75",
"zustand": "^5.0.10"
},
Expand Down
45 changes: 24 additions & 21 deletions src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { loginSchema, LoginFormData } from '../../lib/validationSchemas';
import { FormError, FieldError } from '../../../components/forms/FormError';
import { SubmitButton } from '../../../components/forms/SubmitButton';
import { useMutation } from '../../../hooks/useMutation';
import { apiClient } from '@/lib/api';
import { parseApiError } from '@/utils/error-handler';

export default function LoginPage() {
const [isLoading, setIsLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [apiError, setApiError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const router = useRouter();

Expand All @@ -27,20 +27,20 @@ export default function LoginPage() {
resolver: zodResolver(loginSchema),
});

const onSubmit = async (data: LoginFormData) => {
setIsLoading(true);
setApiError('');
setSuccessMessage('');

try {
const loginMutation = useMutation(
async (data: LoginFormData) => {
await apiClient.post('/api/auth/login', data);
setSuccessMessage('Login successful! Redirecting...');
setTimeout(() => router.push('/dashboard'), 1500);
} catch (error) {
setApiError(parseApiError(error).userMessage);
} finally {
setIsLoading(false);
}
},
{
onSuccess: () => {
setSuccessMessage('Login successful! Redirecting...');
setTimeout(() => router.push('/dashboard'), 1500);
},
},
);

const onSubmit = async (data: LoginFormData) => {
await loginMutation.mutate(data);
};

return (
Expand Down Expand Up @@ -131,7 +131,10 @@ export default function LoginPage() {
</Link>
</div>

<FormError error={apiError} id="login-api-error" />
<FormError
error={loginMutation.error?.message}
id="login-api-error"
/>

{successMessage && (
<motion.div
Expand All @@ -143,13 +146,13 @@ export default function LoginPage() {
</motion.div>
)}

<button
type="submit"
disabled={isLoading}
<SubmitButton
isLoading={loginMutation.isLoading}
loadingText="Signing in…"
className="w-full py-3 bg-gradient-to-r from-cyan-400 to-blue-500 text-white font-semibold rounded-lg hover:from-cyan-500 hover:to-blue-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Signing in...' : 'Sign in'}
</button>
Sign in
</SubmitButton>
</motion.form>

{/* Sign up link */}
Expand Down
45 changes: 24 additions & 21 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { signupSchema, SignupFormData } from '../../lib/validationSchemas';
import { FormError, FieldError } from '../../../components/forms/FormError';
import { SubmitButton } from '../../../components/forms/SubmitButton';
import { useMutation } from '../../../hooks/useMutation';
import { apiClient } from '@/lib/api';
import { parseApiError } from '@/utils/error-handler';

export default function SignupPage() {
const [isLoading, setIsLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [apiError, setApiError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const router = useRouter();

Expand All @@ -27,20 +27,20 @@ export default function SignupPage() {
resolver: zodResolver(signupSchema),
});

const onSubmit = async (data: SignupFormData) => {
setIsLoading(true);
setApiError('');
setSuccessMessage('');

try {
const signupMutation = useMutation(
async (data: SignupFormData) => {
await apiClient.post('/api/auth/signup', data);
setSuccessMessage('Account created successfully! Redirecting...');
setTimeout(() => router.push('/dashboard'), 1500);
} catch (error) {
setApiError(parseApiError(error).userMessage);
} finally {
setIsLoading(false);
}
},
{
onSuccess: () => {
setSuccessMessage('Account created successfully! Redirecting...');
setTimeout(() => router.push('/dashboard'), 1500);
},
},
);

const onSubmit = async (data: SignupFormData) => {
await signupMutation.mutate(data);
};

return (
Expand Down Expand Up @@ -133,7 +133,10 @@ export default function SignupPage() {
<FieldError error={errors.password?.message} id="password-error" />
</div>

<FormError error={apiError} id="signup-api-error" />
<FormError
error={signupMutation.error?.message}
id="signup-api-error"
/>

{successMessage && (
<motion.div
Expand All @@ -145,13 +148,13 @@ export default function SignupPage() {
</motion.div>
)}

<button
type="submit"
disabled={isLoading}
<SubmitButton
isLoading={signupMutation.isLoading}
loadingText="Creating account…"
className="w-full py-3 bg-gradient-to-r from-cyan-400 to-blue-500 text-white font-semibold rounded-lg hover:from-cyan-500 hover:to-blue-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Creating account...' : 'Create account'}
</button>
Create account
</SubmitButton>
</motion.form>

{/* Sign in link */}
Expand Down
68 changes: 37 additions & 31 deletions src/components/forms/DynamicFormBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { FormStateManager } from '@/form-management/state/form-state-manager';
import { ValidationEngineImpl } from '@/form-management/validation/validation-engine';
import { AutoSaveManagerImpl } from '@/form-management/auto-save/auto-save-manager';
import { useNotification } from '@/hooks/use-notification';
import { useMutation } from '@/hooks/useMutation';
import { SubmitButton } from '@/components/forms/SubmitButton';

interface DynamicFormBuilderProps {
config: FormConfiguration | string;
Expand All @@ -38,6 +40,23 @@ export const DynamicFormBuilder: React.FC<DynamicFormBuilderProps> = ({
const [saveStatus, setSaveStatus] = useState<string>('idle');
const { success, error: notifyError } = useNotification();

// ── Mutation ─────────────────────────────────────────────────────────────
const submitMutation = useMutation(
async (values: Record<string, any>) => {
if (onSubmit) {
await onSubmit(values);
}
},
{
onSuccess: () => {
success('Form submitted successfully!');
},
onError: () => {
notifyError('Submission failed. Please try again.');
},
},
);

// Parse configuration
useEffect(() => {
const parser = new FormConfigurationParser();
Expand Down Expand Up @@ -122,36 +141,21 @@ export const DynamicFormBuilder: React.FC<DynamicFormBuilderProps> = ({
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (!formConfig) return;

stateManager.setSubmitting(true);
const toastId = success('Submitting...');

try {
// Validate entire form
const validationResult = await validationEngine.validateForm(stateManager.getState());
if (!formConfig || submitMutation.isLoading) return;

if (!validationResult.isValid) {
notifyError('Validation failed. Please check the required fields.');
stateManager.setSubmitting(false);
return;
}
// Validate entire form before delegating to mutation
const validationResult = await validationEngine.validateForm(stateManager.getState());

// Submit form
if (onSubmit) {
await onSubmit(formState.values);
success('Form submitted successfully!');
}
if (!validationResult.isValid) {
notifyError('Validation failed. Please check the required fields.');
return;
}

// Clear draft after successful submission
if (autoSave) {
await autoSaveManager.clearDraft(formConfig.id);
}
await submitMutation.mutate(formState.values);

stateManager.completeSubmission(true);
} catch (error) {
notifyError('Submission failed. Please try again.');
stateManager.completeSubmission(false);
// Clear draft after successful submission
if (autoSave && submitMutation.isSuccess) {
await autoSaveManager.clearDraft(formConfig.id);
}
};

Expand Down Expand Up @@ -196,6 +200,7 @@ export const DynamicFormBuilder: React.FC<DynamicFormBuilderProps> = ({
onBlur: () => handleFieldBlur(field.id),
placeholder: field.placeholder,
required: field.required,
disabled: submitMutation.isLoading,
className: 'form-input',
};

Expand Down Expand Up @@ -257,13 +262,14 @@ export const DynamicFormBuilder: React.FC<DynamicFormBuilderProps> = ({
</div>

<div className="form-actions">
<button
type="submit"
disabled={formState.isSubmitting || !stateManager.isFormValid()}
<SubmitButton
isLoading={submitMutation.isLoading}
loadingText="Submitting…"
disabled={!stateManager.isFormValid()}
className="btn-submit"
>
{formState.isSubmitting ? 'Submitting...' : 'Submit'}
</button>
Submit
</SubmitButton>
</div>
</form>
);
Expand Down
Loading
Loading