Improved login and sign up
This commit is contained in:
Generated
+31
@@ -17,6 +17,7 @@
|
|||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@types/axios": "^0.9.36",
|
"@types/axios": "^0.9.36",
|
||||||
"@types/date-fns": "^2.5.3",
|
"@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": {
|
"node_modules/@radix-ui/react-tooltip": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@types/axios": "^0.9.36",
|
"@types/axios": "^0.9.36",
|
||||||
"@types/date-fns": "^2.5.3",
|
"@types/date-fns": "^2.5.3",
|
||||||
|
|||||||
+2
-80
@@ -1,85 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import Auth from "@/components/auth/Auth";
|
||||||
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";
|
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [username, setUsername] = useState("");
|
return <Auth kind="login" />;
|
||||||
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 (
|
|
||||||
<div className="h-screen flex items-center">
|
|
||||||
<div className="w-md mx-auto p-7 rounded-2xl flex flex-col gap-4 bg-neutral-900">
|
|
||||||
<h1 className="mx-auto text-3xl">Login</h1>
|
|
||||||
<span>
|
|
||||||
Username or email
|
|
||||||
<Input onChange={(e) => setUsername(e.target.value)} type="email" />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
Password
|
|
||||||
<Input
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") login();
|
|
||||||
}}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Feedback message */}
|
|
||||||
{feedback && (
|
|
||||||
<div
|
|
||||||
className={`text-sm rounded-md p-2 ${
|
|
||||||
feedback.kind === "error"
|
|
||||||
? "bg-red-500/20 text-red-400"
|
|
||||||
: "bg-green-500/20 text-green-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{feedback.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Label>
|
|
||||||
Don't have an account? <Link href={"/signup"}>Sign Up</Link>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Button className="mt-2" onClick={login}>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-145
@@ -1,150 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import Auth from "@/components/auth/Auth";
|
||||||
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";
|
|
||||||
|
|
||||||
export default function SignUp() {
|
export default function SignUp() {
|
||||||
const [username, setUsername] = useState("");
|
return <Auth kind="create" />;
|
||||||
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 (
|
|
||||||
<div className="h-screen flex items-center">
|
|
||||||
<div className="w-md mx-auto p-7 rounded-2xl flex flex-col gap-4 bg-neutral-900 shadow-lg">
|
|
||||||
<h1 className="mx-auto text-3xl font-bold">Create an Account</h1>
|
|
||||||
<span>
|
|
||||||
Email
|
|
||||||
<Input
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
placeholder="[email protected]"
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
Username
|
|
||||||
<Input
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
placeholder="john_doe"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
Password
|
|
||||||
<Input
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
Confirm Password
|
|
||||||
<Input
|
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") signup();
|
|
||||||
}}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={acceptTerms}
|
|
||||||
onChange={(e) => setAcceptTerms(e.target.checked)}
|
|
||||||
/>
|
|
||||||
I agree to the <Link className="underline" href="/terms">terms & conditions</Link>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{/* Feedback message */}
|
|
||||||
{feedback && (
|
|
||||||
<div
|
|
||||||
className={`text-sm rounded-md p-2 ${
|
|
||||||
feedback.kind === "error"
|
|
||||||
? "bg-red-500/20 text-red-400"
|
|
||||||
: "bg-green-500/20 text-green-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{feedback.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button className="mt-4" onClick={signup} disabled={loading}>
|
|
||||||
{loading ? "Signing Up..." : "Sign Up"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="h-screen flex items-center">
|
||||||
|
<Tabs defaultValue={kind} className="mx-auto">
|
||||||
|
<TabsList className="mx-auto">
|
||||||
|
<TabsTrigger value="login">Login</TabsTrigger>
|
||||||
|
<TabsTrigger value="create">Create Account</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="login">
|
||||||
|
<Card className="w-md">
|
||||||
|
<Login />
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="create">
|
||||||
|
<Card className="w-md">
|
||||||
|
<CreateAccount />
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-3xl mx-auto">Create an Account</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="gap-3 flex flex-col">
|
||||||
|
<span>
|
||||||
|
Email
|
||||||
|
<Input
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="[email protected]"
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Username
|
||||||
|
<Input
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="john_doe"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Password
|
||||||
|
<Input
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Confirm Password
|
||||||
|
<Input
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") signup();
|
||||||
|
}}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Label className="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={acceptTerms}
|
||||||
|
onChange={(e) => setAcceptTerms(e.target.checked)}
|
||||||
|
/>
|
||||||
|
I agree to the{" "}
|
||||||
|
<Link className="underline" href="/terms">
|
||||||
|
terms & conditions
|
||||||
|
</Link>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{feedback && (
|
||||||
|
<div
|
||||||
|
className={`text-sm rounded-md p-2 ${
|
||||||
|
feedback.kind === "error"
|
||||||
|
? "bg-red-500/20 text-red-400"
|
||||||
|
: "bg-green-500/20 text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{feedback.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button onClick={signup} disabled={loading}>
|
||||||
|
{loading ? "Signing Up..." : "Sign Up"}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-3xl mx-auto">Login</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="gap-3 flex flex-col">
|
||||||
|
<span>
|
||||||
|
Username or email
|
||||||
|
<Input onChange={(e) => setUsername(e.target.value)} type="email" />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Password
|
||||||
|
<Input
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") login();
|
||||||
|
}}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{feedback && (
|
||||||
|
<div
|
||||||
|
className={`text-sm rounded-md p-2 ${
|
||||||
|
feedback.kind === "error"
|
||||||
|
? "bg-red-500/20 text-red-400"
|
||||||
|
: "bg-green-500/20 text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{feedback.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button className="mt-2" onClick={login}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<typeof TabsPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Root
|
||||||
|
data-slot="tabs"
|
||||||
|
className={cn("flex flex-col gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
data-slot="tabs-list"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
data-slot="tabs-content"
|
||||||
|
className={cn("flex-1 outline-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
Reference in New Issue
Block a user