diff --git a/package-lock.json b/package-lock.json
index 753e2e1..74105af 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@types/axios": "^0.9.36",
"@types/date-fns": "^2.5.3",
@@ -1559,6 +1560,36 @@
}
}
},
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
diff --git a/package.json b/package.json
index b136150..ceb1207 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@types/axios": "^0.9.36",
"@types/date-fns": "^2.5.3",
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index cb098e0..092325a 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -1,85 +1,7 @@
"use client";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import axios from "axios";
-import { useState } from "react";
-import Cookies from "js-cookie";
-import { useRouter } from "next/navigation";
-import { Label } from "@/components/ui/label";
-import Link from "next/link";
+import Auth from "@/components/auth/Auth";
export default function Login() {
- const [username, setUsername] = useState("");
- const [password, setPassword] = useState("");
- const [feedback, setFeedback] = useState<
- undefined | { kind: "error" | "info"; message: string }
- >();
-
- const router = useRouter();
-
- const login = async () => {
- setFeedback(undefined);
- try {
- const res = await axios.put("/api/user", { username, password });
-
- Cookies.set("token", (res.data as any).token);
- setFeedback({
- kind: "info",
- message: "Login successful! Redirecting...",
- });
-
- // small delay to show feedback before redirect
- setTimeout(() => router.push("/home"), 800);
- } catch (err: any) {
- setFeedback({
- kind: "error",
- message:
- err.response?.data?.message || "Login failed. Please try again.",
- });
- }
- };
-
- return (
-
-
-
Login
-
- Username or email
- setUsername(e.target.value)} type="email" />
-
-
- Password
- setPassword(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") login();
- }}
- type="password"
- />
-
-
- {/* Feedback message */}
- {feedback && (
-
- {feedback.message}
-
- )}
-
-
-
-
-
-
- );
+ return ;
}
diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx
index 71ff4c8..4a6a477 100644
--- a/src/app/signup/page.tsx
+++ b/src/app/signup/page.tsx
@@ -1,150 +1,7 @@
"use client";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import axios from "axios";
-import { useState } from "react";
-import Cookies from "js-cookie";
-import { useRouter } from "next/navigation";
-import Link from "next/link";
+import Auth from "@/components/auth/Auth";
export default function SignUp() {
- const [username, setUsername] = useState("");
- const [email, setEmail] = useState("");
- const [password, setPassword] = useState("");
- const [confirmPassword, setConfirmPassword] = useState("");
- const [acceptTerms, setAcceptTerms] = useState(false);
- const [loading, setLoading] = useState(false);
- const [feedback, setFeedback] = useState<
- undefined | { kind: "error" | "info"; message: string }
- >();
-
- const router = useRouter();
-
- const validateInput = ():
- | { kind: "error" | "info"; message: string }
- | undefined => {
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
-
- if (!emailRegex.test(email)) {
- return { kind: "error", message: "Invalid email format." };
- }
- if (!usernameRegex.test(username)) {
- return {
- kind: "error",
- message:
- "Username must be 3–20 characters and only letters, numbers, or underscores.",
- };
- }
- if (password.length < 6) {
- return {
- kind: "error",
- message: "Password must be at least 6 characters long.",
- };
- }
- if (password !== confirmPassword) {
- return { kind: "error", message: "Passwords do not match." };
- }
- if (!acceptTerms) {
- return {
- kind: "error",
- message: "You must accept the terms to sign up.",
- };
- }
- return;
- };
-
- const signup = async () => {
- setFeedback(undefined);
- const validationError = validateInput();
- if (validationError) {
- setFeedback(validationError);
- return;
- }
-
- try {
- setLoading(true);
- const res = await axios.post("/api/user", { username, password, email });
- Cookies.set("token", (res.data as any).token);
-
- setFeedback({ kind: "info", message: "Account created! Redirecting..." });
- setTimeout(() => router.push("/home"), 1000);
- } catch (err: any) {
- setFeedback({
- kind: "error",
- message:
- err.response?.data?.message ||
- "Signup failed. Please try again later.",
- });
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
- );
+ return ;
}
diff --git a/src/components/auth/Auth.tsx b/src/components/auth/Auth.tsx
new file mode 100644
index 0000000..b8e60bc
--- /dev/null
+++ b/src/components/auth/Auth.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { Card } from "../ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
+import CreateAccount from "./CreateAccount";
+import Login from "./Login";
+
+export default function Auth({ kind }: { kind: "login" | "create" }) {
+ return (
+
+
+
+ Login
+ Create Account
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/auth/CreateAccount.tsx b/src/components/auth/CreateAccount.tsx
new file mode 100644
index 0000000..223b69e
--- /dev/null
+++ b/src/components/auth/CreateAccount.tsx
@@ -0,0 +1,156 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import axios from "axios";
+import { useState } from "react";
+import Cookies from "js-cookie";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import { CardContent, CardHeader, CardTitle } from "../ui/card";
+import { Label } from "../ui/label";
+
+export default function CreateAccount() {
+ const [username, setUsername] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [acceptTerms, setAcceptTerms] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [feedback, setFeedback] = useState<
+ undefined | { kind: "error" | "info"; message: string }
+ >();
+
+ const router = useRouter();
+
+ const validateInput = ():
+ | { kind: "error" | "info"; message: string }
+ | undefined => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
+
+ if (!emailRegex.test(email)) {
+ return { kind: "error", message: "Invalid email format." };
+ }
+ if (!usernameRegex.test(username)) {
+ return {
+ kind: "error",
+ message:
+ "Username must be 3–20 characters and only letters, numbers, or underscores.",
+ };
+ }
+ if (password.length < 6) {
+ return {
+ kind: "error",
+ message: "Password must be at least 6 characters long.",
+ };
+ }
+ if (password !== confirmPassword) {
+ return { kind: "error", message: "Passwords do not match." };
+ }
+ if (!acceptTerms) {
+ return {
+ kind: "error",
+ message: "You must accept the terms to sign up.",
+ };
+ }
+ return;
+ };
+
+ const signup = async () => {
+ setFeedback(undefined);
+ const validationError = validateInput();
+ if (validationError) {
+ setFeedback(validationError);
+ return;
+ }
+
+ try {
+ setLoading(true);
+ const res = await axios.post("/api/user", { username, password, email });
+ Cookies.set("token", (res.data as any).token);
+
+ setFeedback({ kind: "info", message: "Account created! Redirecting..." });
+ setTimeout(() => router.push("/home"), 1000);
+ } catch (err: any) {
+ setFeedback({
+ kind: "error",
+ message:
+ err.response?.data?.message ||
+ "Signup failed. Please try again later.",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+ <>
+
+ Create an Account
+
+
+
+ Email
+ setEmail(e.target.value)}
+ placeholder="john@example.com"
+ type="email"
+ />
+
+
+ Username
+ setUsername(e.target.value)}
+ placeholder="john_doe"
+ />
+
+
+ Password
+ setPassword(e.target.value)}
+ type="password"
+ />
+
+
+ Confirm Password
+ setConfirmPassword(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") signup();
+ }}
+ type="password"
+ />
+
+
+
+
+ {feedback && (
+
+ {feedback.message}
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/src/components/auth/Login.tsx b/src/components/auth/Login.tsx
new file mode 100644
index 0000000..3adc91e
--- /dev/null
+++ b/src/components/auth/Login.tsx
@@ -0,0 +1,89 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import axios from "axios";
+import { useState } from "react";
+import Cookies from "js-cookie";
+import { useRouter } from "next/navigation";
+import { Label } from "@/components/ui/label";
+import Link from "next/link";
+import {
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "../ui/card";
+
+export default function Login() {
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const [feedback, setFeedback] = useState<
+ undefined | { kind: "error" | "info"; message: string }
+ >();
+
+ const router = useRouter();
+
+ const login = async () => {
+ setFeedback(undefined);
+ try {
+ const res = await axios.put("/api/user", { username, password });
+
+ Cookies.set("token", (res.data as any).token);
+ setFeedback({
+ kind: "info",
+ message: "Login successful! Redirecting...",
+ });
+
+ // small delay to show feedback before redirect
+ setTimeout(() => router.push("/home"), 800);
+ } catch (err: any) {
+ setFeedback({
+ kind: "error",
+ message:
+ err.response?.data?.message || "Login failed. Please try again.",
+ });
+ }
+ };
+
+ return (
+ <>
+
+ Login
+
+
+
+ Username or email
+ setUsername(e.target.value)} type="email" />
+
+
+ Password
+ setPassword(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") login();
+ }}
+ type="password"
+ />
+
+
+ {feedback && (
+
+ {feedback.message}
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..497ba5e
--- /dev/null
+++ b/src/components/ui/tabs.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }