Merge pull request 'last work on invoice functionality' (#17) from avatar into staging

Reviewed-on: #17
pull/19/head
Subhodip Ghosh 2025-03-28 14:58:36 +00:00
commit 8b75fa057d
3 changed files with 196 additions and 60 deletions

View File

@ -26,6 +26,7 @@ interface AuthResponse {
record: UserRecord;
}
const LoginPage = () => {
const [email, setEmail] = useState('suvodip@siliconpin.com');
const [password, setPassword] = useState('Simple2pass');
@ -35,18 +36,30 @@ const LoginPage = () => {
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
interface AuthResponse {
token: string;
record: {
query: string;
id: string;
email: string;
name: string;
avatar: string;
};
}
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setIsLoading(true);
setStatus({ message: '', isError: false });
try {
const authData = await pb.collection("users").authWithPassword(email, password);
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
const authResponse: AuthResponse = {
token: authData.token,
record: {
query: 'new',
query: 'new',
id: authData.record.id,
email: authData.record.email,
name: authData.record.name || '',
@ -54,8 +67,8 @@ const LoginPage = () => {
}
};
await syncSessionWithBackend(authResponse);
// window.location.href = '/profile';
await syncSessionWithBackend(authResponse, avatarUrl);
window.location.href = '/profile';
} catch (error) {
console.error("Login failed:", error);
setStatus({
@ -77,20 +90,21 @@ const LoginPage = () => {
if (!authData?.record) {
throw new Error("No user record found");
}
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
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 || ''
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';
await syncSessionWithBackend(authResponse, avatarUrl);
window.location.href = '/profile';
} catch (error) {
console.error(`${provider} Login failed:`, error);
setStatus({
@ -101,33 +115,35 @@ const LoginPage = () => {
setIsLoading(false);
}
};
const syncSessionWithBackend = async (authData: AuthResponse) => {
try {
const response = await fetch('http://localhost:2058/host-api/v1/users/session/', {
method: 'POST',
credentials: 'include', // Crucial for cookies
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
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:', data);
} catch (error) {
console.error('Error syncing session:', error);
}
};
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
try {
const response = await fetch('http://localhost:2058/host-api/v1/users/session/', {
method: 'POST',
credentials: 'include', // Important for cookies
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: 'new',
accessToken: authData.token,
email: authData.record.email,
name: authData.record.name,
avatar: avatarUrl,
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);
throw error; // Re-throw the error if you want calling functions to handle it
}
};
return (
<div className="min-h-screen flex items-center justify-center p-4">

View File

@ -0,0 +1,52 @@
import { useState, useRef } from 'react';
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { X } from "lucide-react"; // Import an icon for the remove button
export function AvatarUpload() {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
setSelectedFile(e.target.files[0]);
}
};
const handleRemoveFile = () => {
setSelectedFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = ''; // Reset file input
}
};
return (
<div className="space-y-4">
{!selectedFile ? (
<div className="space-y-2">
<div className="flex items-center gap-4">
<Label
htmlFor="avatar"
className="bg-primary hover:bg-primary/90 text-primary-foreground py-2 px-4 text-sm rounded-md cursor-pointer transition-colorsfocus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
Change Avatar
</Label>
<Input type="file" id="avatar" ref={fileInputRef} accept="image/jpeg,image/png,image/gif" className="hidden" onChange={handleFileChange}/>
<Button variant="outline" size="sm" onClick={() => fileInputRef.current?.click()}>Browse</Button>
</div>
<p className="text-xs text-muted-foreground">
JPG, GIF or PNG. 1MB max.
</p>
</div>
) : (
<div className="flex items-center justify-between p-3 space-x-2">
<div className="truncate max-w-[200px]">
<p className="text-sm font-medium truncate">{selectedFile.name}</p>
<p className="text-xs text-muted-foreground">{(selectedFile.size / 1024).toFixed(2)} KB</p>
</div>
<Button size="sm" className="text-xs p-1 h-fit">Update</Button>
<Button size="sm" onClick={handleRemoveFile} className="bg-red-500 hover:bg-red-600 text-xs p-1 h-fit">Remove</Button>
</div>
)}
</div>
);
}
export default AvatarUpload;

View File

@ -7,6 +7,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from ".
import { Separator } from "./ui/separator";
import { Textarea } from "./ui/textarea";
import React, { useState, useEffect } from 'react';
import UpdateAvatar from './UpdateAvatar';
interface SessionData {
[key: string]: any;
@ -21,6 +22,7 @@ interface UserData {
export default function ProfilePage() {
const [userData, setUserData] = useState<UserData | null>(null);
const [invoiceList, setInvoiceList] = useState<any[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
@ -45,8 +47,36 @@ export default function ProfilePage() {
throw error;
}
};
const getInvoiceListData = async () => {
try {
const response = await fetch('http://localhost:2058/host-api/v1/invoice/invoice-info/', {
method: 'GET',
credentials: 'include', // Crucial for cookies
headers: { 'Accept': 'application/json' }
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.message || 'Session fetch failed');
}
setInvoiceList(data.data); // Fix: Use `data.data` instead of `data`
return data.data; // Fix: `session_data` does not exist in response
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
};
fetchSessionData();
getInvoiceListData();
}, []);
if (error) {
@ -58,12 +88,10 @@ export default function ProfilePage() {
}
return (
<div className="space-y-6 container mx-auto">
<div>
{/* <div>
<h3 className="text-lg font-medium">Profile</h3>
<p className="text-sm text-muted-foreground">
Update your profile settings.
</p>
</div>
<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">
@ -80,14 +108,8 @@ export default function ProfilePage() {
<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>
<UpdateAvatar />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
@ -106,7 +128,7 @@ export default function ProfilePage() {
<Input id="email" type="email" defaultValue={userData.session_data?.user_email || ''} />
</div>
<div className="space-y-2">
{/* <div className="space-y-2">
<Label htmlFor="bio">Bio</Label>
<Textarea
id="bio"
@ -114,11 +136,58 @@ export default function ProfilePage() {
className="resize-none"
rows={5}
/>
</div>
</div> */}
</CardContent>
</Card>
<Card className="mt-6">
<CardHeader>
<CardTitle>Billing Information</CardTitle>
<CardDescription>
View your billing history.
</CardDescription>
{/* <CardContent> */}
<table className="w-full">
<thead>
<tr>
<th className="text-left">Invoice ID</th>
<th className="text-left">Date</th>
<th className="text-left">Description</th>
<th className="text-right">Amount</th>
<th className="text-center">Action</th>
</tr>
</thead>
<tbody>
{/* {
invoiceList.map((invoice) => (
<tr key={invoice.id}>
<td>{invoice.invoice_id}</td>
<td>{invoice.date}</td>
<td>{invoice.description}</td>
<td className="text-right">{invoice.amount}</td>
<td className="text-center"><a href="">Print</a></td>
</tr>
))
} */}
<tr>
<td>dcdicdicib</td>
<td>2023-10-01</td>
<td>Subscription Fee</td>
<td className="text-right">$10.00</td>
<td className="text-center"><a href="">Print</a></td>
</tr>
<tr>
<td>dcibcdici</td>
<td>2023-09-01</td>
<td>Subscription Fee</td>
<td className="text-right">$10.00</td>
<td className="text-center"><a href="" >Print</a></td>
</tr>
</tbody>
</table>
{/* </CardContent> */}
</CardHeader>
</Card>
{/* <Card className="mt-6">
<CardHeader>
<CardTitle>Preferences</CardTitle>
<CardDescription>
@ -156,7 +225,7 @@ export default function ProfilePage() {
</Select>
</div>
</CardContent>
</Card>
</Card> */}
</div>
<div className="flex-1 space-y-6 lg:max-w-md">
@ -183,8 +252,7 @@ export default function ProfilePage() {
<Button className="mt-4">Update password</Button>
</CardContent>
</Card>
<Card>
<Card className="mt-6">
<CardHeader>
<CardTitle>Danger Zone</CardTitle>
<CardDescription>
@ -193,7 +261,7 @@ export default function ProfilePage() {
</CardHeader>
<CardContent>
<div className="flex flex-col space-y-4">
<Button >Delete account</Button>
<Button className="bg-red-500 hover:bg-red-600">Delete account</Button>
<Button variant="outline">Export data</Button>
</div>
</CardContent>