user can upload the avatar or link an url, and show on any toy share pag

This commit is contained in:
Indigo Tang 2025-07-06 12:59:06 +00:00
parent f5706b7487
commit a3f2a71aad
6 changed files with 156 additions and 67 deletions

View File

@ -7,21 +7,13 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Save } from 'lucide-react'; import { Save, Loader2 } from 'lucide-react';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { useI18n, useCurrentLocale, useChangeLocale } from '@/locales/client'; import { useI18n, useCurrentLocale, useChangeLocale } from '@/locales/client';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { getUserByEmail, updateUserProfile } from '@/app/actions/user';
const mockUserProfile = { import type { User } from '@/types';
name: 'Alice Wonderland',
nickname: 'Alice',
email: 'alice@example.com',
avatarUrl: 'https://placehold.co/100x100.png?text=AW',
bio: "Lover of imaginative play and sharing joy. I have a collection of classic storybooks and dress-up costumes.",
phone: '555-123-4567',
location: 'Springfield Gardens, USA',
};
export default function ProfilePage() { export default function ProfilePage() {
const { toast } = useToast(); const { toast } = useToast();
@ -29,22 +21,48 @@ export default function ProfilePage() {
const currentLocale = useCurrentLocale(); const currentLocale = useCurrentLocale();
const changeLocale = useChangeLocale(); const changeLocale = useChangeLocale();
const [name, setName] = useState(mockUserProfile.name); const [user, setUser] = useState<User | null>(null);
const [nickname, setNickname] = useState(mockUserProfile.nickname); const [isFetching, setIsFetching] = useState(true);
const [email, setEmail] = useState(mockUserProfile.email);
const [avatarUrl, setAvatarUrl] = useState(mockUserProfile.avatarUrl); // Form State
const [bio, setBio] = useState(mockUserProfile.bio); const [name, setName] = useState('');
const [phone, setPhone] = useState(mockUserProfile.phone); const [nickname, setNickname] = useState('');
const [location, setLocation] = useState(mockUserProfile.location); const [email, setEmail] = useState('');
const [avatarUrl, setAvatarUrl] = useState('');
const [bio, setBio] = useState('');
// Local state for non-DB fields
const [phone, setPhone] = useState('555-123-4567'); // Mock
const [location, setLocation] = useState('Springfield Gardens, USA'); // Mock
// Other state
const [currentPassword, setCurrentPassword] = useState(''); const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [confirmNewPassword, setConfirmNewPassword] = useState(''); const [confirmNewPassword, setConfirmNewPassword] = useState('');
const [selectedLanguage, setSelectedLanguage] = useState(currentLocale); const [selectedLanguage, setSelectedLanguage] = useState(currentLocale);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isPasswordLoading, setIsPasswordLoading] = useState(false); const [isPasswordLoading, setIsPasswordLoading] = useState(false);
const [isLanguageLoading, setIsLanguageLoading] = useState(false); const [isLanguageLoading, setIsLanguageLoading] = useState(false);
useEffect(() => {
const fetchUser = async () => {
const userEmail = localStorage.getItem('userEmail');
if (userEmail) {
const fetchedUser = await getUserByEmail(userEmail);
if (fetchedUser) {
setUser(fetchedUser);
setName(fetchedUser.name || '');
setNickname(fetchedUser.nickname || '');
setEmail(fetchedUser.email || '');
setAvatarUrl(fetchedUser.avatarUrl || '');
setBio(fetchedUser.bio || '');
}
}
setIsFetching(false);
};
fetchUser();
}, []);
useEffect(() => { useEffect(() => {
setSelectedLanguage(currentLocale); setSelectedLanguage(currentLocale);
@ -52,10 +70,22 @@ export default function ProfilePage() {
const handleProfileUpdate = async (e: React.FormEvent<HTMLFormElement>) => { const handleProfileUpdate = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (!user) return;
setIsLoading(true); setIsLoading(true);
await new Promise(resolve => setTimeout(resolve, 1000));
console.log("Updating profile:", { name, nickname, avatarUrl, bio, phone, location }); const result = await updateUserProfile({
toast({ title: t('dashboard.profile.save_button'), description: "Your profile information has been saved." }); id: user.id,
name,
nickname,
avatarUrl,
bio,
});
if (result.success) {
toast({ title: t('dashboard.profile.save_button'), description: result.message });
} else {
toast({ title: t('admin.users.toast_error_title'), description: result.message, variant: "destructive" });
}
setIsLoading(false); setIsLoading(false);
}; };
@ -70,7 +100,7 @@ export default function ProfilePage() {
return; return;
} }
setIsPasswordLoading(true); setIsPasswordLoading(true);
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000)); // Mock password change
console.log("Changing password"); console.log("Changing password");
toast({ title: t('dashboard.profile.update_password_button'), description: "Your password has been changed successfully." }); toast({ title: t('dashboard.profile.update_password_button'), description: "Your password has been changed successfully." });
setCurrentPassword(''); setCurrentPassword('');
@ -85,11 +115,23 @@ export default function ProfilePage() {
localStorage.setItem('userPreferredLanguage', newLang); localStorage.setItem('userPreferredLanguage', newLang);
setSelectedLanguage(newLang); setSelectedLanguage(newLang);
toast({ title: t('dashboard.profile.language_settings_title'), description: t('dashboard.profile.language_updated_toast') }); toast({ title: t('dashboard.profile.language_settings_title'), description: t('dashboard.profile.language_updated_toast') });
// No need to manually set isLanguageLoading to false if the page reloads/re-renders due to locale change
// However, if changeLocale doesn't cause an immediate unmount, manage loading state appropriately.
// For simplicity, we assume changeLocale will lead to a re-render where currentLocale updates.
}; };
if (isFetching) {
return (
<div className="flex justify-center items-center h-64">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
</div>
);
}
if (!user) {
return (
<div className="text-center py-10 text-muted-foreground">
Could not load user profile. Please try logging in again.
</div>
);
}
return ( return (
<div className="space-y-8 max-w-3xl mx-auto"> <div className="space-y-8 max-w-3xl mx-auto">
@ -140,11 +182,13 @@ export default function ProfilePage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-1"> <div className="space-y-1">
<Label htmlFor="phone">{t('dashboard.profile.phone_label')}</Label> <Label htmlFor="phone">{t('dashboard.profile.phone_label')}</Label>
<Input id="phone" type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Your Phone" disabled={isLoading} /> <Input id="phone" type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Your Phone" disabled={true} />
<p className="text-xs text-muted-foreground">Phone number editing is not yet implemented.</p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Label htmlFor="location">{t('dashboard.profile.location_label')}</Label> <Label htmlFor="location">{t('dashboard.profile.location_label')}</Label>
<Input id="location" value={location} onChange={(e) => setLocation(e.target.value)} placeholder="City, Country" disabled={isLoading} /> <Input id="location" value={location} onChange={(e) => setLocation(e.target.value)} placeholder="City, Country" disabled={true} />
<p className="text-xs text-muted-foreground">Location editing is not yet implemented.</p>
</div> </div>
</div> </div>
</CardContent> </CardContent>

View File

@ -4,7 +4,7 @@ import { getToyById } from '@/data/operations';
import type { Toy } from '@/types'; import type { Toy } from '@/types';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar'; import { Calendar } from '@/components/ui/calendar';
import { ArrowLeft, DollarSign, MapPin, ShoppingBag, UserCircle2 } from 'lucide-react'; import { ArrowLeft, DollarSign, MapPin } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
@ -13,6 +13,8 @@ import type { Locale } from '@/locales/server';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { addDays, parseISO } from 'date-fns'; import { addDays, parseISO } from 'date-fns';
import { getAllToys } from '@/data/operations'; import { getAllToys } from '@/data/operations';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { ShoppingBag } from 'lucide-react';
interface ToyPageProps { interface ToyPageProps {
params: { id: string, locale: Locale }; params: { id: string, locale: Locale };
@ -92,14 +94,19 @@ export default async function ToyPage({ params }: ToyPageProps) {
<Separator /> <Separator />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div className="flex items-center"> <div className="flex items-center gap-2">
<UserCircle2 className="h-5 w-5 mr-2 text-accent" /> <Link href={`/${params.locale}/owner/${toy.ownerId}/toys`} className="flex-shrink-0">
<div> <Avatar className="h-8 w-8">
<span className="font-medium text-muted-foreground">{t('toy_details.owner')}: </span> <AvatarImage src={toy.ownerAvatarUrl} alt={toy.ownerName} data-ai-hint="owner avatar" />
<Link href={`/${params.locale}/owner/${toy.ownerId}/toys`} className="text-foreground hover:underline"> <AvatarFallback>{toy.ownerName.split(' ').map(n => n[0]).join('').toUpperCase()}</AvatarFallback>
{toy.ownerName} </Avatar>
</Link> </Link>
</div> <div>
<span className="font-medium text-muted-foreground">{t('toy_details.owner')}: </span>
<Link href={`/${params.locale}/owner/${toy.ownerId}/toys`} className="text-foreground hover:underline">
{toy.ownerName}
</Link>
</div>
</div> </div>
{toy.location && ( {toy.location && (
<div className="flex items-center"> <div className="flex items-center">

View File

@ -99,7 +99,6 @@ interface DeleteUserResult {
export async function deleteUser(id: string): Promise<DeleteUserResult> { export async function deleteUser(id: string): Promise<DeleteUserResult> {
try { try {
const user = getUserById(id); const user = getUserById(id);
// Prevent deleting the main admin/user accounts for demo stability
if (user && (user.email === 'user@example.com' || user.email === 'admin@example.com')) { if (user && (user.email === 'user@example.com' || user.email === 'admin@example.com')) {
return { success: false, message: 'This is a protected demo account and cannot be deleted.' }; return { success: false, message: 'This is a protected demo account and cannot be deleted.' };
} }
@ -125,3 +124,58 @@ export async function deleteUser(id: string): Promise<DeleteUserResult> {
export async function getUsers(): Promise<User[]> { export async function getUsers(): Promise<User[]> {
return dbGetAllUsers(); return dbGetAllUsers();
} }
export async function getUserByEmail(email: string): Promise<User | null> {
if (!email) {
return null;
}
try {
const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
if (user) {
return user as User;
}
return null;
} catch (error) {
console.error("Error fetching user by email:", error);
return null;
}
}
interface UpdateProfileData {
id: string;
name: string;
nickname?: string;
avatarUrl?: string;
bio?: string;
}
export async function updateUserProfile(data: UpdateProfileData): Promise<{ success: boolean; message: string; }> {
const { id, name, nickname, avatarUrl, bio } = data;
if (!id || !name) {
return { success: false, message: 'User ID and name are required.' };
}
try {
const stmt = db.prepare(
'UPDATE users SET name = @name, nickname = @nickname, avatarUrl = @avatarUrl, bio = @bio WHERE id = @id'
);
stmt.run({
id,
name,
nickname: nickname ?? null,
avatarUrl: avatarUrl ?? '',
bio: bio ?? ''
});
revalidatePath(`/dashboard/profile`);
revalidatePath(`/owner/${id}/toys`);
return { success: true, message: 'Profile updated successfully.' };
} catch (error) {
console.error('Update profile error:', error);
return { success: false, message: 'An unexpected error occurred while updating your profile.' };
}
}

View File

@ -1,7 +1,6 @@
import db from '@/lib/db'; import db from '@/lib/db';
import type { Toy, User } from '@/types'; import type { Toy, User } from '@/types';
import { mockOwnerProfiles } from '@/lib/mockData';
// Helper to parse toy data from DB, converting JSON strings back to objects // Helper to parse toy data from DB, converting JSON strings back to objects
const parseToy = (toyData: any): Toy => { const parseToy = (toyData: any): Toy => {
@ -11,6 +10,7 @@ const parseToy = (toyData: any): Toy => {
images: toyData.images ? JSON.parse(toyData.images) : [], images: toyData.images ? JSON.parse(toyData.images) : [],
unavailableRanges: toyData.unavailableRanges ? JSON.parse(toyData.unavailableRanges) : [], unavailableRanges: toyData.unavailableRanges ? JSON.parse(toyData.unavailableRanges) : [],
pricePerDay: Number(toyData.pricePerDay), pricePerDay: Number(toyData.pricePerDay),
ownerAvatarUrl: toyData.ownerAvatarUrl,
dataAiHint: toyData.category?.toLowerCase() || 'toy' dataAiHint: toyData.category?.toLowerCase() || 'toy'
}; };
}; };
@ -19,7 +19,7 @@ const parseToy = (toyData: any): Toy => {
export function getAllToys(): Toy[] { export function getAllToys(): Toy[] {
const stmt = db.prepare(` const stmt = db.prepare(`
SELECT t.*, u.name as ownerName SELECT t.*, u.name as ownerName, u.avatarUrl as ownerAvatarUrl
FROM toys t FROM toys t
JOIN users u ON t.ownerId = u.id JOIN users u ON t.ownerId = u.id
`); `);
@ -29,7 +29,7 @@ export function getAllToys(): Toy[] {
export function getToyById(id: string): Toy | undefined { export function getToyById(id: string): Toy | undefined {
const stmt = db.prepare(` const stmt = db.prepare(`
SELECT t.*, u.name as ownerName SELECT t.*, u.name as ownerName, u.avatarUrl as ownerAvatarUrl
FROM toys t FROM toys t
JOIN users u ON t.ownerId = u.id JOIN users u ON t.ownerId = u.id
WHERE t.id = ? WHERE t.id = ?
@ -40,7 +40,7 @@ export function getToyById(id: string): Toy | undefined {
export function getToysByOwner(ownerId: string): Toy[] { export function getToysByOwner(ownerId: string): Toy[] {
const stmt = db.prepare(` const stmt = db.prepare(`
SELECT t.*, u.name as ownerName SELECT t.*, u.name as ownerName, u.avatarUrl as ownerAvatarUrl
FROM toys t FROM toys t
JOIN users u ON t.ownerId = u.id JOIN users u ON t.ownerId = u.id
WHERE t.ownerId = ? WHERE t.ownerId = ?
@ -49,9 +49,9 @@ export function getToysByOwner(ownerId: string): Toy[] {
return toys.map(parseToy); return toys.map(parseToy);
} }
// For now, we keep the mock profiles for the owner page bio/avatar // This now fetches the full user profile from the database
export function getOwnerProfile(ownerId: string) { export function getOwnerProfile(ownerId: string): User | undefined {
return mockOwnerProfiles[ownerId] ?? null; return getUserById(ownerId);
} }
// --- USER OPERATIONS --- // --- USER OPERATIONS ---

View File

@ -8,13 +8,13 @@ export const rawUsers: User[] = [
{ id: 'user1', name: 'Alice Wonderland', nickname: 'Alice', email: 'user@example.com', role: 'Admin', avatarUrl: 'https://placehold.co/100x100.png?text=AW', bio: "Lover of imaginative play and sharing joy. I have a collection of classic storybooks and dress-up costumes that my kids have outgrown but still have lots of life left in them!" }, { id: 'user1', name: 'Alice Wonderland', nickname: 'Alice', email: 'user@example.com', role: 'Admin', avatarUrl: 'https://placehold.co/100x100.png?text=AW', bio: "Lover of imaginative play and sharing joy. I have a collection of classic storybooks and dress-up costumes that my kids have outgrown but still have lots of life left in them!" },
{ id: 'user2', name: 'Bob The Builder', nickname: 'Bob', email: 'user2@example.com', role: 'User', avatarUrl: 'https://placehold.co/100x100.png?text=BT', bio: "Can we fix it? Yes, we can! Sharing my collection of construction toys, tools, and playsets. Always happy to help another budding builder." }, { id: 'user2', name: 'Bob The Builder', nickname: 'Bob', email: 'user2@example.com', role: 'User', avatarUrl: 'https://placehold.co/100x100.png?text=BT', bio: "Can we fix it? Yes, we can! Sharing my collection of construction toys, tools, and playsets. Always happy to help another budding builder." },
{ id: 'user3', name: 'Carol Danvers', nickname: 'Captain Marvel', email: 'user3@example.com', role: 'User', avatarUrl: 'https://placehold.co/100x100.png?text=CD', bio: "Higher, further, faster. Sharing toys that inspire adventure, courage, and exploration. My collection includes superhero action figures and space-themed playsets." }, { id: 'user3', name: 'Carol Danvers', nickname: 'Captain Marvel', email: 'user3@example.com', role: 'User', avatarUrl: 'https://placehold.co/100x100.png?text=CD', bio: "Higher, further, faster. Sharing toys that inspire adventure, courage, and exploration. My collection includes superhero action figures and space-themed playsets." },
{ id: 'user4', name: 'Charlie Brown', nickname: 'Chuck', email: 'user4@example.com', role: 'User' }, { id: 'user4', name: 'Charlie Brown', nickname: 'Chuck', email: 'user4@example.com', role: 'User', avatarUrl: '', bio: '' },
{ id: 'user5', name: 'Diana Prince', nickname: 'Wonder Woman', email: 'user5@example.com', role: 'User' }, { id: 'user5', name: 'Diana Prince', nickname: 'Wonder Woman', email: 'user5@example.com', role: 'User', avatarUrl: '', bio: '' },
{ id: 'user6', name: 'Edward Nigma', nickname: 'Riddler', email: 'user6@example.com', role: 'User' }, { id: 'user6', name: 'Edward Nigma', nickname: 'Riddler', email: 'user6@example.com', role: 'User', avatarUrl: '', bio: '' },
{ id: 'admin-main', name: 'Main Admin', nickname: 'Head Honcho', email: 'admin@example.com', role: 'Admin' }, { id: 'admin-main', name: 'Main Admin', nickname: 'Head Honcho', email: 'admin@example.com', role: 'Admin', avatarUrl: 'https://placehold.co/100x100.png?text=ADM', bio: 'Keeping the toy box tidy.' },
]; ];
export const rawToys: Omit<Toy, 'ownerName' | 'dataAiHint'>[] = [ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[] = [
{ {
id: '1', id: '1',
name: 'Colorful Building Blocks Set', name: 'Colorful Building Blocks Set',
@ -97,20 +97,3 @@ export const mockToys = []; // Legacy, will be removed later
export const mockRentalHistory: RentalHistoryEntry[] = []; export const mockRentalHistory: RentalHistoryEntry[] = [];
export const mockRentalRequests: RentalRequest[] = []; export const mockRentalRequests: RentalRequest[] = [];
export const mockMessages: (MessageEntry & { rentalRequestId: string })[] = []; export const mockMessages: (MessageEntry & { rentalRequestId: string })[] = [];
export const mockOwnerProfiles: Record<string, { avatarUrl: string; bio: string; name?: string }> = {
'user1': {
name: 'Alice W.',
avatarUrl: 'https://placehold.co/100x100.png?text=AW',
bio: "Lover of imaginative play and sharing joy. I have a collection of classic storybooks and dress-up costumes that my kids have outgrown but still have lots of life left in them!"
},
'user2': {
name: 'Bob T.B.',
avatarUrl: 'https://placehold.co/100x100.png?text=BT',
bio: "Can we fix it? Yes, we can! Sharing my collection of construction toys, tools, and playsets. Always happy to help another budding builder."
},
'user3': {
name: 'Captain C.',
avatarUrl: 'https://placehold.co/100x100.png?text=CD',
bio: "Higher, further, faster. Sharing toys that inspire adventure, courage, and exploration. My collection includes superhero action figures and space-themed playsets."
}
};

View File

@ -16,6 +16,7 @@ export interface Toy {
unavailableRanges: { startDate: string; endDate: string }[]; unavailableRanges: { startDate: string; endDate: string }[];
ownerName: string; ownerName: string;
ownerId: string; ownerId: string;
ownerAvatarUrl?: string;
pricePerDay?: number; pricePerDay?: number;
location?: string; location?: string;
dataAiHint?: string; dataAiHint?: string;