Skip to content

Commit 4aab8d2

Browse files
File changes
1 parent 225ac40 commit 4aab8d2

File tree

4 files changed

+639
-185
lines changed

4 files changed

+639
-185
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { useState } from 'react';
2+
import { Button } from '@/components/ui/button';
3+
import { Input } from '@/components/ui/input';
4+
import { Textarea } from '@/components/ui/textarea';
5+
import { Label } from '@/components/ui/label';
6+
import { Badge } from '@/components/ui/badge';
7+
import { X, Plus } from 'lucide-react';
8+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
9+
10+
export default function ProfileEditForm({ profile, onSave, onCancel }) {
11+
const [formData, setFormData] = useState({
12+
bio: profile?.bio || '',
13+
skills: profile?.skills || [],
14+
interests: profile?.interests || [],
15+
});
16+
17+
const [newSkill, setNewSkill] = useState({ skill_name: '', proficiency: 'intermediate' });
18+
const [newInterest, setNewInterest] = useState('');
19+
20+
const addSkill = () => {
21+
if (!newSkill.skill_name.trim()) return;
22+
setFormData({
23+
...formData,
24+
skills: [...formData.skills, { ...newSkill }]
25+
});
26+
setNewSkill({ skill_name: '', proficiency: 'intermediate' });
27+
};
28+
29+
const removeSkill = (index) => {
30+
setFormData({
31+
...formData,
32+
skills: formData.skills.filter((_, i) => i !== index)
33+
});
34+
};
35+
36+
const addInterest = () => {
37+
if (!newInterest.trim()) return;
38+
setFormData({
39+
...formData,
40+
interests: [...formData.interests, newInterest.trim()]
41+
});
42+
setNewInterest('');
43+
};
44+
45+
const removeInterest = (index) => {
46+
setFormData({
47+
...formData,
48+
interests: formData.interests.filter((_, i) => i !== index)
49+
});
50+
};
51+
52+
const handleSubmit = (e) => {
53+
e.preventDefault();
54+
onSave(formData);
55+
};
56+
57+
return (
58+
<form onSubmit={handleSubmit} className="space-y-6">
59+
{/* Bio */}
60+
<Card>
61+
<CardHeader>
62+
<CardTitle className="text-lg">About Me</CardTitle>
63+
</CardHeader>
64+
<CardContent>
65+
<Textarea
66+
placeholder="Tell us about yourself..."
67+
value={formData.bio}
68+
onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
69+
rows={4}
70+
className="resize-none"
71+
/>
72+
</CardContent>
73+
</Card>
74+
75+
{/* Skills */}
76+
<Card>
77+
<CardHeader>
78+
<CardTitle className="text-lg">Skills</CardTitle>
79+
</CardHeader>
80+
<CardContent className="space-y-4">
81+
{/* Existing Skills */}
82+
<div className="flex flex-wrap gap-2">
83+
{formData.skills.map((skill, index) => (
84+
<Badge
85+
key={index}
86+
variant="outline"
87+
className="px-3 py-1.5 text-sm"
88+
>
89+
{skill.skill_name}
90+
<span className="ml-2 text-xs text-slate-500">({skill.proficiency})</span>
91+
<button
92+
type="button"
93+
onClick={() => removeSkill(index)}
94+
className="ml-2 hover:text-red-600"
95+
>
96+
<X className="h-3 w-3" />
97+
</button>
98+
</Badge>
99+
))}
100+
</div>
101+
102+
{/* Add New Skill */}
103+
<div className="flex gap-2">
104+
<Input
105+
placeholder="Skill name"
106+
value={newSkill.skill_name}
107+
onChange={(e) => setNewSkill({ ...newSkill, skill_name: e.target.value })}
108+
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addSkill())}
109+
/>
110+
<select
111+
value={newSkill.proficiency}
112+
onChange={(e) => setNewSkill({ ...newSkill, proficiency: e.target.value })}
113+
className="px-3 py-2 border border-slate-300 rounded-md text-sm"
114+
>
115+
<option value="beginner">Beginner</option>
116+
<option value="intermediate">Intermediate</option>
117+
<option value="advanced">Advanced</option>
118+
<option value="expert">Expert</option>
119+
</select>
120+
<Button type="button" onClick={addSkill} size="icon" variant="outline">
121+
<Plus className="h-4 w-4" />
122+
</Button>
123+
</div>
124+
</CardContent>
125+
</Card>
126+
127+
{/* Interests */}
128+
<Card>
129+
<CardHeader>
130+
<CardTitle className="text-lg">Interests</CardTitle>
131+
</CardHeader>
132+
<CardContent className="space-y-4">
133+
{/* Existing Interests */}
134+
<div className="flex flex-wrap gap-2">
135+
{formData.interests.map((interest, index) => (
136+
<Badge
137+
key={index}
138+
variant="secondary"
139+
className="px-3 py-1.5 text-sm"
140+
>
141+
{interest}
142+
<button
143+
type="button"
144+
onClick={() => removeInterest(index)}
145+
className="ml-2 hover:text-red-600"
146+
>
147+
<X className="h-3 w-3" />
148+
</button>
149+
</Badge>
150+
))}
151+
</div>
152+
153+
{/* Add New Interest */}
154+
<div className="flex gap-2">
155+
<Input
156+
placeholder="Add an interest..."
157+
value={newInterest}
158+
onChange={(e) => setNewInterest(e.target.value)}
159+
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addInterest())}
160+
/>
161+
<Button type="button" onClick={addInterest} size="icon" variant="outline">
162+
<Plus className="h-4 w-4" />
163+
</Button>
164+
</div>
165+
</CardContent>
166+
</Card>
167+
168+
{/* Actions */}
169+
<div className="flex justify-end gap-3">
170+
<Button type="button" variant="outline" onClick={onCancel}>
171+
Cancel
172+
</Button>
173+
<Button type="submit" className="bg-int-orange hover:bg-int-orange-dark">
174+
Save Changes
175+
</Button>
176+
</div>
177+
</form>
178+
);
179+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useState, useRef } from 'react';
2+
import { useMutation } from '@tanstack/react-query';
3+
import { base44 } from '@/api/base44Client';
4+
import { Button } from '@/components/ui/button';
5+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
6+
import { Camera, Loader2 } from 'lucide-react';
7+
import { toast } from 'sonner';
8+
9+
export default function ProfilePictureUpload({ currentImageUrl, userEmail, onUploadSuccess }) {
10+
const [uploading, setUploading] = useState(false);
11+
const fileInputRef = useRef(null);
12+
13+
const uploadMutation = useMutation({
14+
mutationFn: async (file) => {
15+
setUploading(true);
16+
17+
// Upload file to storage
18+
const { file_url } = await base44.integrations.Core.UploadFile({ file });
19+
20+
return file_url;
21+
},
22+
onSuccess: (fileUrl) => {
23+
onUploadSuccess(fileUrl);
24+
toast.success('Profile picture updated');
25+
setUploading(false);
26+
},
27+
onError: (error) => {
28+
toast.error('Failed to upload image');
29+
setUploading(false);
30+
}
31+
});
32+
33+
const handleFileChange = (e) => {
34+
const file = e.target.files?.[0];
35+
if (!file) return;
36+
37+
// Validate file type
38+
if (!file.type.startsWith('image/')) {
39+
toast.error('Please select an image file');
40+
return;
41+
}
42+
43+
// Validate file size (max 10MB)
44+
if (file.size > 10 * 1024 * 1024) {
45+
toast.error('Image must be less than 10MB');
46+
return;
47+
}
48+
49+
uploadMutation.mutate(file);
50+
};
51+
52+
return (
53+
<div className="relative inline-block">
54+
<Avatar className="h-32 w-32 border-4 border-white shadow-lg">
55+
<AvatarImage src={currentImageUrl} />
56+
<AvatarFallback className="text-4xl bg-gradient-purple text-white">
57+
{userEmail?.charAt(0).toUpperCase()}
58+
</AvatarFallback>
59+
</Avatar>
60+
61+
<Button
62+
size="icon"
63+
variant="outline"
64+
className="absolute bottom-0 right-0 h-10 w-10 rounded-full bg-white shadow-lg hover:bg-slate-50"
65+
onClick={() => fileInputRef.current?.click()}
66+
disabled={uploading}
67+
>
68+
{uploading ? (
69+
<Loader2 className="h-4 w-4 animate-spin" />
70+
) : (
71+
<Camera className="h-4 w-4" />
72+
)}
73+
</Button>
74+
75+
<input
76+
ref={fileInputRef}
77+
type="file"
78+
accept="image/*"
79+
className="hidden"
80+
onChange={handleFileChange}
81+
/>
82+
</div>
83+
);
84+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { base44 } from '@/api/base44Client';
3+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
4+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
5+
import { Clock } from 'lucide-react';
6+
import { formatDistanceToNow } from 'date-fns';
7+
import { Link } from 'react-router-dom';
8+
import { createPageUrl } from '@/utils';
9+
10+
export default function RecentlyViewedProfiles({ currentUserEmail }) {
11+
const { data: recentViews = [] } = useQuery({
12+
queryKey: ['recent-profile-views', currentUserEmail],
13+
queryFn: async () => {
14+
const views = await base44.entities.ProfileView.filter(
15+
{ viewer_email: currentUserEmail },
16+
'-viewed_at',
17+
10
18+
);
19+
20+
// Get unique profiles (most recent view per profile)
21+
const uniqueViews = [];
22+
const seenEmails = new Set();
23+
24+
for (const view of views) {
25+
if (!seenEmails.has(view.viewed_profile_email)) {
26+
seenEmails.add(view.viewed_profile_email);
27+
uniqueViews.push(view);
28+
}
29+
}
30+
31+
return uniqueViews.slice(0, 5);
32+
},
33+
enabled: !!currentUserEmail
34+
});
35+
36+
// Fetch profile data for recently viewed users
37+
const { data: profiles = [] } = useQuery({
38+
queryKey: ['recently-viewed-profiles-data', recentViews.map(v => v.viewed_profile_email)],
39+
queryFn: async () => {
40+
const emails = recentViews.map(v => v.viewed_profile_email);
41+
if (emails.length === 0) return [];
42+
43+
const profiles = await Promise.all(
44+
emails.map(async (email) => {
45+
const [profile] = await base44.entities.UserProfile.filter({ user_email: email });
46+
return profile || { user_email: email };
47+
})
48+
);
49+
50+
return profiles;
51+
},
52+
enabled: recentViews.length > 0
53+
});
54+
55+
if (recentViews.length === 0) {
56+
return null;
57+
}
58+
59+
return (
60+
<Card>
61+
<CardHeader>
62+
<CardTitle className="flex items-center gap-2 text-lg">
63+
<Clock className="h-5 w-5 text-slate-600" />
64+
Recently Viewed
65+
</CardTitle>
66+
</CardHeader>
67+
<CardContent>
68+
<div className="space-y-3">
69+
{recentViews.map((view, index) => {
70+
const profile = profiles.find(p => p.user_email === view.viewed_profile_email);
71+
if (!profile) return null;
72+
73+
return (
74+
<Link
75+
key={view.id}
76+
to={createPageUrl('UserProfile') + `?email=${view.viewed_profile_email}`}
77+
className="flex items-center gap-3 p-2 rounded-lg hover:bg-slate-50 transition-colors"
78+
>
79+
<Avatar className="h-10 w-10 border-2 border-slate-200">
80+
<AvatarImage src={profile.profile_picture_url} />
81+
<AvatarFallback className="bg-gradient-purple text-white text-sm">
82+
{view.viewed_profile_email.charAt(0).toUpperCase()}
83+
</AvatarFallback>
84+
</Avatar>
85+
86+
<div className="flex-1 min-w-0">
87+
<p className="text-sm font-medium text-slate-900 truncate">
88+
{view.viewed_profile_email}
89+
</p>
90+
{profile.role && (
91+
<p className="text-xs text-slate-500 truncate">{profile.role}</p>
92+
)}
93+
</div>
94+
95+
<span className="text-xs text-slate-400 whitespace-nowrap">
96+
{formatDistanceToNow(new Date(view.viewed_at), { addSuffix: true })}
97+
</span>
98+
</Link>
99+
);
100+
})}
101+
</div>
102+
</CardContent>
103+
</Card>
104+
);
105+
}

0 commit comments

Comments
 (0)