Skip to content

Commit ee85f50

Browse files
committed
feat(env): segregate dev/pro environment
1 parent c19a66d commit ee85f50

File tree

10 files changed

+618
-18
lines changed

10 files changed

+618
-18
lines changed

compose.dev.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
services:
2+
frontend-dev:
3+
build:
4+
context: .
5+
dockerfile: Dockerfile
6+
container_name: frontend-dev
7+
ports:
8+
- "3000:3000"
9+
environment:
10+
- PORT=3000
11+
- NODE_ENV=production
12+
- SPRING_BOOT_API_URL=${SPRING_BOOT_API_URL}
13+
networks:
14+
- heapdog-network-dev
15+
16+
networks:
17+
heapdog-network-dev:
18+
external: true
19+

compose.yml

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,9 @@
11
services:
2-
frontend-dev:
3-
build:
4-
context: .
5-
dockerfile: Dockerfile.dev
6-
working_dir: /app
7-
container_name: nextjs-app
8-
volumes:
9-
- .:/app
10-
- /app/node_modules # prevent node_modules from being overwritten
11-
ports:
12-
- "3000:3000"
13-
command: ["npm", "run", "dev"]
14-
networks:
15-
- heapdog-network
16-
172
frontend:
183
build:
194
context: .
205
dockerfile: Dockerfile
21-
container_name: nextjs-app-prod
6+
container_name: frontend-prod
227
ports:
238
- "5050:5050"
249
environment:
@@ -28,7 +13,6 @@ services:
2813
networks:
2914
- heapdog-network
3015

31-
3216
networks:
3317
heapdog-network:
3418
external: true

src/app/api/auth/signup/route.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { ApiErrorResponse } from "@/lib/types/api";
3+
import { SignupRequest, SignupResponse } from "@/lib/types/auth";
4+
import { BackendClient } from "@/lib/backend-client";
5+
6+
export async function POST(request: NextRequest) {
7+
try {
8+
const body: SignupRequest = await request.json();
9+
10+
// Proxies to Spring Boot POST /users/signup
11+
const response = await BackendClient.post<SignupResponse>("/users/signup", body);
12+
13+
return NextResponse.json(response, { status: 200 });
14+
} catch (error) {
15+
console.error("Signup error:", error);
16+
17+
const errorResponse = error as ApiErrorResponse;
18+
19+
return NextResponse.json(
20+
errorResponse,
21+
{ status: errorResponse.status || 500 }
22+
);
23+
}
24+
}
25+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { ApiErrorResponse } from "@/lib/types/api";
3+
import { VerifyEmailRequest, VerifyEmailResponse } from "@/lib/types/auth";
4+
import { BackendClient } from "@/lib/backend-client";
5+
6+
export async function POST(request: NextRequest) {
7+
try {
8+
const body: VerifyEmailRequest = await request.json();
9+
10+
// Proxies to Spring Boot POST /users/verify-email
11+
const response = await BackendClient.post<VerifyEmailResponse>("/users/verify-email", body);
12+
13+
return NextResponse.json(response, { status: 200 });
14+
} catch (error) {
15+
console.error("Verify email error:", error);
16+
17+
const errorResponse = error as ApiErrorResponse;
18+
19+
return NextResponse.json(
20+
errorResponse,
21+
{ status: errorResponse.status || 500 }
22+
);
23+
}
24+
}
25+

src/app/globals.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@
174174
top: 4.5rem !important; /* Adjust spinner position if visible */
175175
right: 1rem !important;
176176
}
177-
178177
@layer base {
179178
* {
180179
@apply border-border outline-ring/50;
@@ -183,3 +182,4 @@
183182
@apply bg-background text-foreground;
184183
}
185184
}
185+

src/app/signup/page.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { getCurrentUser } from "@/lib/auth";
2+
import { redirect } from "next/navigation";
3+
import SignupPage from "./signup-page";
4+
import { Metadata } from "next";
5+
6+
export const metadata: Metadata = {
7+
title: "Sign Up | HeapDog",
8+
description: "Create your HeapDog account",
9+
};
10+
11+
export default async function Page() {
12+
const user = await getCurrentUser();
13+
14+
if (user) {
15+
redirect("/");
16+
}
17+
18+
return <SignupPage />;
19+
}
20+

src/app/signup/signup-page.tsx

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
"use client";
2+
3+
import { zodResolver } from "@hookform/resolvers/zod";
4+
import { useForm } from "react-hook-form";
5+
import * as z from "zod";
6+
import Link from "next/link";
7+
import { useRouter } from "next/navigation";
8+
import { User, Mail, Key, Loader2, AlertCircle, UserPlus } from "lucide-react";
9+
import { useMutation } from "@tanstack/react-query";
10+
11+
import { Button } from "@/components/ui/button";
12+
import {
13+
Form,
14+
FormControl,
15+
FormField,
16+
FormItem,
17+
FormLabel,
18+
FormMessage,
19+
} from "@/components/ui/form";
20+
import { Input } from "@/components/ui/input";
21+
import {
22+
Card,
23+
CardContent,
24+
CardDescription,
25+
CardFooter,
26+
CardHeader,
27+
CardTitle,
28+
} from "@/components/ui/card";
29+
import { SignupRequest, SignupResponse } from "@/lib/types/auth";
30+
import { ApiErrorResponse, ApiResponse } from "@/lib/types/api";
31+
32+
const formSchema = z.object({
33+
username: z.string().min(3, {
34+
message: "Username must be at least 3 characters.",
35+
}),
36+
email: z.string().email({
37+
message: "Please enter a valid email address.",
38+
}),
39+
password: z.string().min(4, {
40+
message: "Password must be at least 4 characters.",
41+
}),
42+
});
43+
44+
export default function SignupPage() {
45+
const router = useRouter();
46+
const form = useForm<z.infer<typeof formSchema>>({
47+
resolver: zodResolver(formSchema),
48+
defaultValues: {
49+
username: "",
50+
email: "",
51+
password: "",
52+
},
53+
});
54+
55+
const mutation = useMutation<
56+
ApiResponse<SignupResponse>,
57+
ApiErrorResponse,
58+
SignupRequest
59+
>({
60+
mutationFn: async (data) => {
61+
const res = await fetch("/api/auth/signup", {
62+
method: "POST",
63+
headers: {
64+
"Content-Type": "application/json",
65+
},
66+
body: JSON.stringify(data),
67+
});
68+
69+
if (!res.ok) {
70+
const errorData: ApiErrorResponse = await res.json();
71+
throw errorData;
72+
}
73+
74+
return res.json();
75+
},
76+
onSuccess: (response) => {
77+
// Redirect to verify page with user ID
78+
if (response.data && response.data.id) {
79+
router.push(`/verify?user=${response.data.id}`);
80+
} else {
81+
// Fallback if ID is missing (shouldn't happen based on spec)
82+
router.push("/signin");
83+
}
84+
},
85+
onError: (error) => {
86+
console.error("Signup error:", error);
87+
if (error.details && error.details.length > 0) {
88+
error.details.forEach((detail) => {
89+
const fieldName = detail.field as keyof z.infer<typeof formSchema>;
90+
// Check if field exists in form (backend might return 'id' or other fields)
91+
if (["username", "email", "password"].includes(fieldName)) {
92+
form.setError(fieldName, {
93+
type: "server",
94+
message: detail.message,
95+
});
96+
} else {
97+
// If field doesn't match form fields, set root error
98+
form.setError("root", {
99+
type: "server",
100+
message: detail.message
101+
});
102+
}
103+
});
104+
} else {
105+
form.setError("root", {
106+
type: "server",
107+
message: error.message || "Registration failed. Please try again.",
108+
});
109+
}
110+
},
111+
});
112+
113+
function onSubmit(values: z.infer<typeof formSchema>) {
114+
mutation.mutate(values);
115+
}
116+
117+
return (
118+
<div className="flex min-h-[calc(100vh-4rem)] flex-col items-center justify-center py-8 px-4 sm:px-6 lg:px-8">
119+
<Card className="w-full max-w-md border-border bg-card shadow-2xl py-4 sm:py-6">
120+
<CardHeader className="space-y-1 text-center px-4 sm:px-6">
121+
<CardTitle className="text-2xl font-bold tracking-tight">
122+
Create an Account
123+
</CardTitle>
124+
<CardDescription className="text-muted-foreground text-sm sm:text-base">
125+
Join HeapDog to start your journey.
126+
</CardDescription>
127+
</CardHeader>
128+
<CardContent className="px-4 sm:px-6">
129+
<Form {...form}>
130+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
131+
{form.formState.errors.root && (
132+
<div className="bg-destructive/15 text-destructive text-sm p-3 rounded-md flex items-center gap-2">
133+
<AlertCircle className="h-4 w-4" />
134+
<p>{form.formState.errors.root.message}</p>
135+
</div>
136+
)}
137+
138+
<FormField
139+
control={form.control}
140+
name="username"
141+
render={({ field }) => (
142+
<FormItem>
143+
<FormLabel className="text-foreground/90">
144+
Username
145+
</FormLabel>
146+
<FormControl>
147+
<div className="relative">
148+
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" size={16} />
149+
<Input
150+
placeholder="johndoe"
151+
className="pl-10 h-11 sm:h-10 text-base sm:text-sm bg-background border-input focus-visible:ring-primary transition-all duration-200"
152+
{...field}
153+
disabled={mutation.isPending}
154+
/>
155+
</div>
156+
</FormControl>
157+
<FormMessage />
158+
</FormItem>
159+
)}
160+
/>
161+
162+
<FormField
163+
control={form.control}
164+
name="email"
165+
render={({ field }) => (
166+
<FormItem>
167+
<FormLabel className="text-foreground/90">
168+
Email
169+
</FormLabel>
170+
<FormControl>
171+
<div className="relative">
172+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" size={16} />
173+
<Input
174+
type="email"
175+
placeholder="john@example.com"
176+
className="pl-10 h-11 sm:h-10 text-base sm:text-sm bg-background border-input focus-visible:ring-primary transition-all duration-200"
177+
{...field}
178+
disabled={mutation.isPending}
179+
/>
180+
</div>
181+
</FormControl>
182+
<FormMessage />
183+
</FormItem>
184+
)}
185+
/>
186+
187+
<FormField
188+
control={form.control}
189+
name="password"
190+
render={({ field }) => (
191+
<FormItem>
192+
<FormLabel className="text-foreground/90">
193+
Password
194+
</FormLabel>
195+
<FormControl>
196+
<div className="relative">
197+
<Key className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" size={16} />
198+
<Input
199+
type="password"
200+
placeholder="••••••••"
201+
className="pl-10 h-11 sm:h-10 text-base sm:text-sm bg-background border-input focus-visible:ring-primary transition-all duration-200"
202+
{...field}
203+
disabled={mutation.isPending}
204+
/>
205+
</div>
206+
</FormControl>
207+
<FormMessage />
208+
</FormItem>
209+
)}
210+
/>
211+
212+
<Button
213+
type="submit"
214+
className="w-full h-11 sm:h-10 text-base sm:text-sm bg-primary text-primary-foreground hover:brightness-110 active:scale-[0.98] transition-all duration-200 ease-in-out font-semibold cursor-pointer shadow-sm"
215+
disabled={mutation.isPending}
216+
>
217+
{mutation.isPending ? (
218+
<>
219+
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Creating Account...
220+
</>
221+
) : (
222+
<>
223+
<UserPlus className="mr-2 h-4 w-4" /> Sign Up
224+
</>
225+
)}
226+
</Button>
227+
</form>
228+
</Form>
229+
</CardContent>
230+
<CardFooter className="flex flex-col space-y-4 pt-0 text-center px-4 sm:px-6">
231+
<div className="relative w-full">
232+
<div className="absolute inset-0 flex items-center">
233+
<span className="w-full border-t border-border" />
234+
</div>
235+
<div className="relative flex justify-center text-xs uppercase">
236+
<span className="bg-card px-2 text-muted-foreground whitespace-nowrap">
237+
Already have an account?
238+
</span>
239+
</div>
240+
</div>
241+
<Button
242+
variant="outline"
243+
asChild
244+
className="w-full h-11 sm:h-10 text-base sm:text-sm border-border hover:bg-accent hover:text-accent-foreground active:scale-[0.98] transition-all duration-200 ease-in-out cursor-pointer text-foreground"
245+
>
246+
<Link href="/signin">
247+
Sign In
248+
</Link>
249+
</Button>
250+
</CardFooter>
251+
</Card>
252+
</div>
253+
);
254+
}
255+

0 commit comments

Comments
 (0)