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 ( -
-
-

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 message */} - {feedback && ( -
- {feedback.message} -
- )} - - -
-
- ); + 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 }