user can upload the avatar or link an url, and show on any toy share pag
This commit is contained in:
parent
f5706b7487
commit
a3f2a71aad
|
|
@ -7,21 +7,13 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
|||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
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 { Textarea } from '@/components/ui/textarea';
|
||||
import { useI18n, useCurrentLocale, useChangeLocale } from '@/locales/client';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
||||
const mockUserProfile = {
|
||||
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',
|
||||
};
|
||||
import { getUserByEmail, updateUserProfile } from '@/app/actions/user';
|
||||
import type { User } from '@/types';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { toast } = useToast();
|
||||
|
|
@ -29,22 +21,48 @@ export default function ProfilePage() {
|
|||
const currentLocale = useCurrentLocale();
|
||||
const changeLocale = useChangeLocale();
|
||||
|
||||
const [name, setName] = useState(mockUserProfile.name);
|
||||
const [nickname, setNickname] = useState(mockUserProfile.nickname);
|
||||
const [email, setEmail] = useState(mockUserProfile.email);
|
||||
const [avatarUrl, setAvatarUrl] = useState(mockUserProfile.avatarUrl);
|
||||
const [bio, setBio] = useState(mockUserProfile.bio);
|
||||
const [phone, setPhone] = useState(mockUserProfile.phone);
|
||||
const [location, setLocation] = useState(mockUserProfile.location);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isFetching, setIsFetching] = useState(true);
|
||||
|
||||
// Form State
|
||||
const [name, setName] = useState('');
|
||||
const [nickname, setNickname] = useState('');
|
||||
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 [newPassword, setNewPassword] = useState('');
|
||||
const [confirmNewPassword, setConfirmNewPassword] = useState('');
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(currentLocale);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isPasswordLoading, setIsPasswordLoading] = 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(() => {
|
||||
setSelectedLanguage(currentLocale);
|
||||
|
|
@ -52,10 +70,22 @@ export default function ProfilePage() {
|
|||
|
||||
const handleProfileUpdate = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!user) return;
|
||||
setIsLoading(true);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
console.log("Updating profile:", { name, nickname, avatarUrl, bio, phone, location });
|
||||
toast({ title: t('dashboard.profile.save_button'), description: "Your profile information has been saved." });
|
||||
|
||||
const result = await updateUserProfile({
|
||||
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);
|
||||
};
|
||||
|
||||
|
|
@ -70,7 +100,7 @@ export default function ProfilePage() {
|
|||
return;
|
||||
}
|
||||
setIsPasswordLoading(true);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Mock password change
|
||||
console.log("Changing password");
|
||||
toast({ title: t('dashboard.profile.update_password_button'), description: "Your password has been changed successfully." });
|
||||
setCurrentPassword('');
|
||||
|
|
@ -85,11 +115,23 @@ export default function ProfilePage() {
|
|||
localStorage.setItem('userPreferredLanguage', newLang);
|
||||
setSelectedLanguage(newLang);
|
||||
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 (
|
||||
<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="space-y-1">
|
||||
<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 className="space-y-1">
|
||||
<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>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { getToyById } from '@/data/operations';
|
|||
import type { Toy } from '@/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { Badge } from '@/components/ui/badge';
|
||||
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 { addDays, parseISO } from 'date-fns';
|
||||
import { getAllToys } from '@/data/operations';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { ShoppingBag } from 'lucide-react';
|
||||
|
||||
interface ToyPageProps {
|
||||
params: { id: string, locale: Locale };
|
||||
|
|
@ -92,14 +94,19 @@ export default async function ToyPage({ params }: ToyPageProps) {
|
|||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex items-center">
|
||||
<UserCircle2 className="h-5 w-5 mr-2 text-accent" />
|
||||
<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}
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href={`/${params.locale}/owner/${toy.ownerId}/toys`} className="flex-shrink-0">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={toy.ownerAvatarUrl} alt={toy.ownerName} data-ai-hint="owner avatar" />
|
||||
<AvatarFallback>{toy.ownerName.split(' ').map(n => n[0]).join('').toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
</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>
|
||||
{toy.location && (
|
||||
<div className="flex items-center">
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ interface DeleteUserResult {
|
|||
export async function deleteUser(id: string): Promise<DeleteUserResult> {
|
||||
try {
|
||||
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')) {
|
||||
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[]> {
|
||||
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.' };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import db from '@/lib/db';
|
||||
import type { Toy, User } from '@/types';
|
||||
import { mockOwnerProfiles } from '@/lib/mockData';
|
||||
|
||||
// Helper to parse toy data from DB, converting JSON strings back to objects
|
||||
const parseToy = (toyData: any): Toy => {
|
||||
|
|
@ -11,6 +10,7 @@ const parseToy = (toyData: any): Toy => {
|
|||
images: toyData.images ? JSON.parse(toyData.images) : [],
|
||||
unavailableRanges: toyData.unavailableRanges ? JSON.parse(toyData.unavailableRanges) : [],
|
||||
pricePerDay: Number(toyData.pricePerDay),
|
||||
ownerAvatarUrl: toyData.ownerAvatarUrl,
|
||||
dataAiHint: toyData.category?.toLowerCase() || 'toy'
|
||||
};
|
||||
};
|
||||
|
|
@ -19,7 +19,7 @@ const parseToy = (toyData: any): Toy => {
|
|||
|
||||
export function getAllToys(): Toy[] {
|
||||
const stmt = db.prepare(`
|
||||
SELECT t.*, u.name as ownerName
|
||||
SELECT t.*, u.name as ownerName, u.avatarUrl as ownerAvatarUrl
|
||||
FROM toys t
|
||||
JOIN users u ON t.ownerId = u.id
|
||||
`);
|
||||
|
|
@ -29,7 +29,7 @@ export function getAllToys(): Toy[] {
|
|||
|
||||
export function getToyById(id: string): Toy | undefined {
|
||||
const stmt = db.prepare(`
|
||||
SELECT t.*, u.name as ownerName
|
||||
SELECT t.*, u.name as ownerName, u.avatarUrl as ownerAvatarUrl
|
||||
FROM toys t
|
||||
JOIN users u ON t.ownerId = u.id
|
||||
WHERE t.id = ?
|
||||
|
|
@ -40,7 +40,7 @@ export function getToyById(id: string): Toy | undefined {
|
|||
|
||||
export function getToysByOwner(ownerId: string): Toy[] {
|
||||
const stmt = db.prepare(`
|
||||
SELECT t.*, u.name as ownerName
|
||||
SELECT t.*, u.name as ownerName, u.avatarUrl as ownerAvatarUrl
|
||||
FROM toys t
|
||||
JOIN users u ON t.ownerId = u.id
|
||||
WHERE t.ownerId = ?
|
||||
|
|
@ -49,9 +49,9 @@ export function getToysByOwner(ownerId: string): Toy[] {
|
|||
return toys.map(parseToy);
|
||||
}
|
||||
|
||||
// For now, we keep the mock profiles for the owner page bio/avatar
|
||||
export function getOwnerProfile(ownerId: string) {
|
||||
return mockOwnerProfiles[ownerId] ?? null;
|
||||
// This now fetches the full user profile from the database
|
||||
export function getOwnerProfile(ownerId: string): User | undefined {
|
||||
return getUserById(ownerId);
|
||||
}
|
||||
|
||||
// --- USER OPERATIONS ---
|
||||
|
|
|
|||
|
|
@ -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: '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: 'user4', name: 'Charlie Brown', nickname: 'Chuck', email: 'user4@example.com', role: 'User' },
|
||||
{ id: 'user5', name: 'Diana Prince', nickname: 'Wonder Woman', email: 'user5@example.com', role: 'User' },
|
||||
{ id: 'user6', name: 'Edward Nigma', nickname: 'Riddler', email: 'user6@example.com', role: 'User' },
|
||||
{ id: 'admin-main', name: 'Main Admin', nickname: 'Head Honcho', email: 'admin@example.com', role: 'Admin' },
|
||||
{ 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', avatarUrl: '', bio: '' },
|
||||
{ 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', 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',
|
||||
name: 'Colorful Building Blocks Set',
|
||||
|
|
@ -97,20 +97,3 @@ export const mockToys = []; // Legacy, will be removed later
|
|||
export const mockRentalHistory: RentalHistoryEntry[] = [];
|
||||
export const mockRentalRequests: RentalRequest[] = [];
|
||||
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."
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export interface Toy {
|
|||
unavailableRanges: { startDate: string; endDate: string }[];
|
||||
ownerName: string;
|
||||
ownerId: string;
|
||||
ownerAvatarUrl?: string;
|
||||
pricePerDay?: number;
|
||||
location?: string;
|
||||
dataAiHint?: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue