diff --git a/backend/apps/authentications/api/serializers.py b/backend/apps/authentications/api/serializers.py index 40c5867..d4080de 100644 --- a/backend/apps/authentications/api/serializers.py +++ b/backend/apps/authentications/api/serializers.py @@ -117,4 +117,4 @@ def validate(self, attrs): raise serializers.ValidationError( {"confirm_password": "Passwords do not match."} ) - return attrs + return attrs \ No newline at end of file diff --git a/backend/apps/authentications/api/views.py b/backend/apps/authentications/api/views.py index 4923907..5bc029d 100644 --- a/backend/apps/authentications/api/views.py +++ b/backend/apps/authentications/api/views.py @@ -168,4 +168,4 @@ def post(self, request): return Response( APIResponse.get_response(message="User created successfully.", data=data), status=status.HTTP_201_CREATED, - ) + ) \ No newline at end of file diff --git a/backend/core/views.py b/backend/core/views.py index 14a0985..1e4e945 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -6,6 +6,7 @@ class HealthCheckAPI(APIView): permission_classes = (AllowAny,) + def get(self, request): status = {"status": "OK"} return Response(status) diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..5934e2e --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:8000 diff --git a/frontend/app/components/HealthCheck.tsx b/frontend/app/components/HealthCheck.tsx new file mode 100644 index 0000000..1d28f6a --- /dev/null +++ b/frontend/app/components/HealthCheck.tsx @@ -0,0 +1,40 @@ +/** + * HealthCheck — presentational component that displays backend health status. + * + * Receives data as props from the route's clientLoader. + * No useEffect, no useState, no fetch — just renders what it's given. + */ + +import { CenteredPageLayout } from "~/components/layout/CenteredPageLayout"; + +const statusStyles: Record = { + ok: "text-green-600 dark:text-green-400", + error: "text-red-600 dark:text-red-400", +}; + +interface HealthCheckProps { + ok: boolean; + detail: string; +} + +export function HealthCheck({ ok, detail }: HealthCheckProps) { + const status = ok ? "ok" : "error"; + + return ( + +

+ Backend health check +

+ +

+ {ok ? "✅ Backend is healthy" : "❌ Backend unreachable"} +

+ + {detail && ( +
+          {detail}
+        
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/app/components/LoginForm.tsx b/frontend/app/components/LoginForm.tsx new file mode 100644 index 0000000..450e431 --- /dev/null +++ b/frontend/app/components/LoginForm.tsx @@ -0,0 +1,64 @@ +/** + * LoginForm — presentational component for the login page. + * + * All mutation logic lives in the route's clientAction. + * This component just renders the form UI and displays errors. + */ + +import { Form, Link } from "react-router"; +import { CenteredPageLayout } from "~/components/layout/CenteredPageLayout"; +import { TextField } from "~/components/ui/TextField"; + +interface LoginFormProps { + error?: string; + isSubmitting: boolean; +} + +export function LoginForm({ error, isSubmitting }: LoginFormProps) { + return ( + +

+ Log in +

+ +
+ + + + {error && ( +

{error}

+ )} + + + + +

+ Don't have an account?{" "} + + Sign up + +

+
+ ); +} \ No newline at end of file diff --git a/frontend/app/components/SignupForm.tsx b/frontend/app/components/SignupForm.tsx new file mode 100644 index 0000000..986bff3 --- /dev/null +++ b/frontend/app/components/SignupForm.tsx @@ -0,0 +1,71 @@ +/** + * SignupForm — presentational component for the signup page. + * + * All mutation logic lives in the route's clientAction. + * This component just renders the form UI and displays errors. + */ + +import { Form, Link } from "react-router"; +import { CenteredPageLayout } from "~/components/layout/CenteredPageLayout"; +import { TextField } from "~/components/ui/TextField"; + +interface SignupFormProps { + error?: string; + isSubmitting: boolean; +} + +export function SignupForm({ error, isSubmitting }: SignupFormProps) { + return ( + +

+ Sign up +

+ +
+ + + + + {error && ( +

{error}

+ )} + + + + +

+ Already have an account?{" "} + + Log in + +

+
+ ); +} \ No newline at end of file diff --git a/frontend/app/components/layout/CenteredPageLayout.tsx b/frontend/app/components/layout/CenteredPageLayout.tsx new file mode 100644 index 0000000..aa0cf1f --- /dev/null +++ b/frontend/app/components/layout/CenteredPageLayout.tsx @@ -0,0 +1,26 @@ +/** + * Shared page layout — centered card on a full-height background. + * + * Used by login, signup, health, and any future standalone pages. + */ + +interface CenteredPageLayoutProps { + children: React.ReactNode; + /** Tailwind max-width class, defaults to "max-w-sm" */ + maxWidth?: string; +} + +export function CenteredPageLayout({ + children, + maxWidth = "max-w-sm", +}: CenteredPageLayoutProps) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/frontend/app/components/ui/TextField.tsx b/frontend/app/components/ui/TextField.tsx new file mode 100644 index 0000000..b9777be --- /dev/null +++ b/frontend/app/components/ui/TextField.tsx @@ -0,0 +1,54 @@ +/** + * Reusable text input with proper accessibility. + * + * - Renders