This commit is contained in:
Suvodip
2025-03-27 19:19:47 +05:30
parent d2f3576d10
commit beabcc7b67
17 changed files with 1242 additions and 29 deletions

266
src/components/Login.tsx Normal file
View File

@@ -0,0 +1,266 @@
import { useState } from 'react';
import type { FormEvent } from 'react';
import PocketBase from 'pocketbase';
import { Input } from "./ui/input";
import { Button } from "./ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { Eye, EyeOff, Loader2 } from "lucide-react";
import { Separator } from "./ui/separator";
import { Label } from "./ui/label";
interface AuthStatus {
message: string;
isError: boolean;
}
interface UserRecord {
id: string;
email: string;
name?: string;
avatar?: string;
[key: string]: any;
}
interface AuthResponse {
token: string;
record: UserRecord;
}
const LoginPage = () => {
const [email, setEmail] = useState('suvodip@siliconpin.com');
const [password, setPassword] = useState('Simple2pass');
const [passwordVisible, setPasswordVisible] = useState(false);
const [status, setStatus] = useState<AuthStatus>({ message: '', isError: false });
const [isLoading, setIsLoading] = useState(false);
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setIsLoading(true);
setStatus({ message: '', isError: false });
try {
const authData = await pb.collection("users").authWithPassword(email, password);
const authResponse: AuthResponse = {
token: authData.token,
record: {
query: 'new',
id: authData.record.id,
email: authData.record.email,
name: authData.record.name || '',
avatar: authData.record.avatar || ''
}
};
await syncSessionWithBackend(authResponse);
// window.location.href = '/profile';
} catch (error) {
console.error("Login failed:", error);
setStatus({
message: "Login failed. Please check your credentials.",
isError: true
});
} finally {
setIsLoading(false);
}
};
const loginWithOAuth2 = async (provider: 'google' | 'facebook' | 'github') => {
try {
setIsLoading(true);
setStatus({ message: '', isError: false });
const authData = await pb.collection('users').authWithOAuth2({ provider });
if (!authData?.record) {
throw new Error("No user record found");
}
const authResponse: AuthResponse = {
token: authData.token,
record: {
query: 'new',
id: authData.record.id,
email: authData.record.email || '',
name: authData.record.name || '',
avatar: authData.record.avatar || ''
}
};
await syncSessionWithBackend(authResponse);
// window.location.href = '/profile';
} catch (error) {
console.error(`${provider} Login failed:`, error);
setStatus({
message: `${provider} login failed. Please try again.`,
isError: true
});
} finally {
setIsLoading(false);
}
};
const syncSessionWithBackend = async (authData: AuthResponse) => {
try {
const response = await fetch('http://localhost:2058/host-api/v1/users/session/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: 'new',
accessToken: authData.token,
email: authData.record.email,
name: authData.record.name,
avatar: authData.record.avatar
? pb.files.getUrl(authData.record, authData.record.avatar)
: '',
isAuthenticated: true,
id: authData.record.id
})
});
if (!response.ok) {
throw new Error('Failed to sync session');
}
const data = await response.json();
console.log('Session synced with backend:', data);
} catch (error) {
console.error('Error syncing session:', error);
}
};
return (
<div className="min-h-screen flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-lg rounded-xl overflow-hidden">
<CardHeader className="text-center space-y-1">
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
<CardDescription className="">
Sign in to access your account
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{status.message && (
<div className={`p-3 rounded-md text-sm ${status.isError ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
{status.message}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email" className="">
Email Address
</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
className="focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label htmlFor="password" className="">
Password
</Label>
<button
type="button"
onClick={() => setPasswordVisible(!passwordVisible)}
className="text-sm text-[#6d9e37]"
>
</button>
</div>
<div className="relative">
<Input
id="password"
type={passwordVisible ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="focus:ring-2 focus:ring-blue-500 pr-10"
/>
<button
type="button"
onClick={() => setPasswordVisible(!passwordVisible)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{passwordVisible ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<Button
type="submit"
disabled={isLoading}
className="w-full"
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing in...
</>
) : (
'Sign In'
)}
</Button>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<Separator className="w-full" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<Button
variant="outline"
onClick={() => loginWithOAuth2('google')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/google.svg" alt="Google" className="h-6 w-6" />
</Button>
<Button
variant="outline"
onClick={() => loginWithOAuth2('facebook')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/facebook.svg" alt="Facebook" className="h-6 w-6" />
</Button>
<Button
variant="outline"
onClick={() => loginWithOAuth2('github')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/github.svg" alt="GitHub" className="h-6 w-6" />
</Button>
</div>
</CardContent>
<div className="px-6 pb-6 text-center text-sm text-gray-500">
Don't have an account?{' '}
<a href="/sign-up" className="font-medium text-[#6d9e37] hover:underline">
Sign up
</a>
</div>
</Card>
</div>
);
};
export default LoginPage;

View File

@@ -0,0 +1,210 @@
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
import { Button } from "./ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "./ui/select";
import { Separator } from "./ui/separator";
import { Textarea } from "./ui/textarea";
import React, { useState, useEffect } from 'react';
interface SessionData {
[key: string]: any;
}
interface UserData {
success: boolean;
session_data: SessionData;
user_avatar: string;
}
export default function ProfilePage() {
const [userData, setUserData] = useState<UserData | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
'http://localhost:2058/host-api/v1/users/get-profile-data/',
{
credentials: 'include',
headers: {
'Accept': 'application/json',
}
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: UserData = await response.json();
// console.log('success message', data.success);
if(data.success === true){
setUserData(data);
// console.log('User Data', data);
}
} catch (error) {
console.error('Fetch error:', error);
setError(error instanceof Error ? error.message : 'An unknown error occurred');
}
};
fetchData();
}, []);
if (error) {
return <div>Error: {error}</div>;
}
if (!userData) {
return <div>Loading profile data...</div>;
}
return (
<div className="space-y-6 container mx-auto">
<div>
<h3 className="text-lg font-medium">Profile</h3>
<p className="text-sm text-muted-foreground">
Update your profile settings.
</p>
</div>
<Separator />
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<div className="flex-1 lg:max-w-2xl">
<Card>
<CardHeader>
<CardTitle>Personal Information</CardTitle>
<CardDescription>
Update your personal information and avatar.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center space-x-4">
<Avatar className="h-16 w-16">
<AvatarImage src={userData.session_data?.user_avatar} />
<AvatarFallback>JP</AvatarFallback>
</Avatar>
<div className="space-y-1">
<Button size="sm">
Change avatar
</Button>
<p className="text-xs text-muted-foreground">
JPG, GIF or PNG. 1MB max.
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="firstName">Full Name</Label>
<Input id="firstName" defaultValue={userData.session_data?.user_name || 'Jhon'} />
</div>
<div className="space-y-2">
<Label htmlFor="phone">Phone</Label>
<Input id="phone" defaultValue={userData.session_data?.user_vatar || ''} />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" defaultValue={userData.session_data?.user_email || ''} />
</div>
<div className="space-y-2">
<Label htmlFor="bio">Bio</Label>
<Textarea
id="bio"
defaultValue="I'm a software developer based in New York."
className="resize-none"
rows={5}
/>
</div>
</CardContent>
</Card>
<Card className="mt-6">
<CardHeader>
<CardTitle>Preferences</CardTitle>
<CardDescription>
Configure your application preferences.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="language">Language</Label>
<Select defaultValue="english">
<SelectTrigger id="language">
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent>
<SelectItem value="english">English</SelectItem>
<SelectItem value="french">French</SelectItem>
<SelectItem value="german">German</SelectItem>
<SelectItem value="spanish">Spanish</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="timezone">Timezone</Label>
<Select defaultValue="est">
<SelectTrigger id="timezone">
<SelectValue placeholder="Select timezone" />
</SelectTrigger>
<SelectContent>
<SelectItem value="est">Eastern Standard Time (EST)</SelectItem>
<SelectItem value="cst">Central Standard Time (CST)</SelectItem>
<SelectItem value="mst">Mountain Standard Time (MST)</SelectItem>
<SelectItem value="pst">Pacific Standard Time (PST)</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
</div>
<div className="flex-1 space-y-6 lg:max-w-md">
<Card>
<CardHeader>
<CardTitle>Security</CardTitle>
<CardDescription>
Update your password and security settings.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="currentPassword">Current password</Label>
<Input id="currentPassword" type="password" />
</div>
<div className="space-y-2">
<Label htmlFor="newPassword">New password</Label>
<Input id="newPassword" type="password" />
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm password</Label>
<Input id="confirmPassword" type="password" />
</div>
<Button className="mt-4">Update password</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Danger Zone</CardTitle>
<CardDescription>
These actions are irreversible. Proceed with caution.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col space-y-4">
<Button >Delete account</Button>
<Button variant="outline">Export data</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,57 @@
import * as React from "react"
import { cn } from "../../lib/utils"
const Avatar = React.forwardRef<
HTMLSpanElement,
React.HTMLAttributes<HTMLSpanElement> & {
size?: "sm" | "md" | "lg"
}
>(({ className, size = "md", ...props }, ref) => {
const sizeClasses = {
sm: "h-8 w-8",
md: "h-10 w-10",
lg: "h-12 w-12",
};
return (
<span
ref={ref}
className={cn(
"relative flex shrink-0 overflow-hidden rounded-full",
sizeClasses[size],
className
)}
{...props}
/>
)
})
Avatar.displayName = "Avatar"
const AvatarImage = React.forwardRef<
HTMLImageElement,
React.ImgHTMLAttributes<HTMLImageElement>
>(({ className, ...props }, ref) => (
<img
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = "AvatarImage"
const AvatarFallback = React.forwardRef<
HTMLSpanElement,
React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
<span
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = "AvatarFallback"
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -8,7 +8,7 @@ export interface ButtonProps
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", size = "default", ...props }, ref) => {
({ className, variant = "default", type = '', size = "default", ...props }, ref) => {
return (
<button
className={cn(

View File

@@ -1,25 +1,130 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "../../lib/utils";
export interface SelectProps
extends React.SelectHTMLAttributes<HTMLSelectElement> {}
const Select = SelectPrimitive.Root;
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, ...props }, ref) => {
return (
<select
className={cn(
"flex h-10 w-full rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm ring-offset-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
>
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm",
"focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:ring-offset-2 focus:ring-offset-neutral-900",
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-neutral-600 bg-neutral-800 shadow-md animate-in fade-in-80",
className
)}
{...props}
>
<SelectPrimitive.Viewport className="p-1">
{children}
</select>
);
}
);
Select.displayName = "Select";
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
export { Select };
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
"focus:bg-neutral-700 focus:text-white",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-neutral-600", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
// ChevronDownIcon component
const ChevronDownIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => (
<svg
ref={ref}
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
));
ChevronDownIcon.displayName = "ChevronDownIcon";
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
};

View File

@@ -0,0 +1,23 @@
import * as React from "react"
import { cn } from "../../lib/utils"
const Separator = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
orientation?: "horizontal" | "vertical"
}
>(({ className, orientation = "horizontal", ...props }, ref) => (
<div
ref={ref}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
role="separator"
{...props}
/>
))
Separator.displayName = "Separator"
export { Separator }