Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avatar modal #238

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
72 changes: 10 additions & 62 deletions imports/ui/components/headers/ProfileCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { FaUserPlus, FaPencilAlt } from 'react-icons/fa';
import ProfileDropdown from '../profileDropdown/ProfileDropdown';
import AvatarModal from '../../modals/AvatarModal';

const ProfileCard = React.memo(({ user, showFollowButton, currentUser, isFollowing, isRequested, toggleFollow }) => {
const [showAvatarModal, setShowAvatarModal] = useState(false);
Expand Down Expand Up @@ -32,8 +33,7 @@ const ProfileCard = React.memo(({ user, showFollowButton, currentUser, isFollowi
setShowAvatarModal(true);
};

const handleAvatarChange = (e) => {
const file = e.target.files[0];
const handleAvatarChange = (file) => {
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
Expand All @@ -46,9 +46,10 @@ const ProfileCard = React.memo(({ user, showFollowButton, currentUser, isFollowi
}
});
};
reader.readAsDataURL(file);
reader.readAsDataURL(file);
}
};


const handlePresetAvatarSelect = (avatarUrl) => {
Meteor.call("updateAvatar", user._id, avatarUrl, (error) => {
Expand Down Expand Up @@ -89,18 +90,6 @@ const ProfileCard = React.memo(({ user, showFollowButton, currentUser, isFollowi
});
};

const presetAvatars = [
'https://randomuser.me/api/portraits/lego/1.jpg',
'https://randomuser.me/api/portraits/lego/2.jpg',
'https://randomuser.me/api/portraits/lego/3.jpg',
"https://randomuser.me/api/portraits/lego/4.jpg",
"https://randomuser.me/api/portraits/lego/5.jpg",
"https://randomuser.me/api/portraits/lego/6.jpg",
"https://randomuser.me/api/portraits/lego/7.jpg",
"https://randomuser.me/api/portraits/lego/8.jpg",
"https://randomuser.me/api/portraits/lego/9.jpg",
];

return (
<div className="relative flex items-center h-72 p-4 bg-gradient-to-tl from-zinc-900 via-zinc-700 to-zinc-600 rounded-t-lg shadow-md">
<div className="absolute top-4 right-4">
Expand Down Expand Up @@ -227,53 +216,12 @@ const ProfileCard = React.memo(({ user, showFollowButton, currentUser, isFollowi

{/* Avatar Modal */}
{showAvatarModal && (
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-zinc-700 p-6 rounded-lg shadow-lg max-w-md w-full mt-20 z-50">
<h3 className="text-lg font-bold mb-4 text-center">
Change Profile Picture
</h3>
<input
type="file"
accept="image/*"
onChange={handleAvatarChange}
className="mb-4 block w-full text-sm text-white file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-gray-200 file:text-gray-700 hover:file:bg-gray-300"
/>
<div className="grid grid-cols-5 gap-4 mb-4">
{presetAvatars.slice(0, 5).map((avatarUrl, index) => (
<img
key={index}
src={avatarUrl}
alt="preset avatar"
className="w-16 h-16 object-cover rounded-full cursor-pointer transition-transform transform hover:scale-110"
onClick={() => handlePresetAvatarSelect(avatarUrl)}
/>
))}
</div>
<div className="flex justify-center">
<div
className="grid grid-cols-4 gap-4 mb-4"
style={{ marginLeft: "0%" }}
>
{presetAvatars.slice(5).map((avatarUrl, index) => (
<img
key={index}
src={avatarUrl}
alt="preset avatar"
className="w-16 h-16 object-cover rounded-full cursor-pointer transition-transform transform hover:scale-110"
onClick={() => handlePresetAvatarSelect(avatarUrl)}
/>
))}
</div>
</div>
<div className="flex justify-end">
<button
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
onClick={() => setShowAvatarModal(false)}
>
Cancel
</button>
</div>
</div>
<div style={{ zIndex: 1000, position: 'relative' }}>
<AvatarModal
handleAvatarChange={handleAvatarChange}
handlePresetAvatarSelect={handlePresetAvatarSelect}
setShowAvatarModal={setShowAvatarModal}
/>
</div>
)}
</div>
Expand Down
195 changes: 195 additions & 0 deletions imports/ui/modals/AvatarModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import React, { useState, useRef, useEffect } from 'react';
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';

const AvatarModal = ({ handleAvatarChange, setShowAvatarModal }) => {
const [selectedFaceIndex, setSelectedFaceIndex] = useState(0);
const [selectedHairIndex, setSelectedHairIndex] = useState(0);
const [selectedBackgroundIndex, setSelectedBackgroundIndex] = useState(0);
const canvasRef = useRef(null);

const expressions = [
{ id: "face1", label: "1", image: "/images/frown.png" },
{ id: "face2", label: "2", image: "/images/grr.png" },
{ id: "face3", label: "2", image: "/images/smile.png" },
{ id: "face4", label: "2", image: "/images/smirk.png" }
];

const hairstyles = [
{ id: "hair1", label: "1", image: "/images/short.png" },
{ id: "hair2", label: "2", image: "/images/long.png" },
{ id: "hair3", label: "2", image: "/images/spiky.png" }
];

const backgrounds = [
{ id: "bg1", label: "1", image: "/images/orangebg.png" },
{ id: "bg2", label: "2", image: "/images/greenbg.png" },
{ id: "bg3", label: "3", image: "/images/pinkbg.png" }
];

useEffect(() => {
renderToCanvas();
}, [selectedFaceIndex, selectedHairIndex, selectedBackgroundIndex]);

const renderToCanvas = () => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");

const faceImage = new Image();
faceImage.src = expressions[selectedFaceIndex].image;

const hairImage = new Image();
hairImage.src = hairstyles[selectedHairIndex].image;

const backgroundImage = new Image();
backgroundImage.src = backgrounds[selectedBackgroundIndex].image;

// Load all images using Promise.all
Promise.all([
new Promise((resolve) => (backgroundImage.onload = resolve)),
new Promise((resolve) => (faceImage.onload = resolve)),
new Promise((resolve) => (hairImage.onload = resolve))
]).then(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);

// Draw the background first
ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);

// Face scaling and aspect ratio calculation
const faceScale = 0.8;
const faceAspectRatio = faceImage.width / faceImage.height;
let faceWidth, faceHeight;

if (canvas.width / canvas.height > faceAspectRatio) {
faceHeight = canvas.height * faceScale;
faceWidth = faceHeight * faceAspectRatio;
} else {
faceWidth = canvas.width * faceScale;
faceHeight = faceWidth / faceAspectRatio;
}

const faceXOffset = (canvas.width - faceWidth) / 2;
const faceYOffset = (canvas.height - faceHeight) / 2;
ctx.drawImage(faceImage, faceXOffset, faceYOffset, faceWidth, faceHeight);

// Hair scaling and aspect ratio preservation
const hairScale = 0.9;
const hairAspectRatio = hairImage.width / hairImage.height;
let hairWidth, hairHeight;

if (canvas.width / canvas.height > hairAspectRatio) {
hairHeight = (canvas.height * hairScale);
hairWidth = hairHeight * hairAspectRatio;
} else {
hairWidth = (canvas.width * hairScale);
hairHeight = hairWidth / hairAspectRatio;
}

const hairXOffset = (canvas.width - hairWidth) / 2;
const hairYOffset = (canvas.height - hairHeight) / 2 - hairHeight / 3.9;
ctx.drawImage(hairImage, hairXOffset, hairYOffset, hairWidth, hairHeight);
});
};

const handleNextFace = () => {
setSelectedFaceIndex((prevIndex) => (prevIndex + 1) % expressions.length);
};

const handlePreviousFace = () => {
setSelectedFaceIndex((prevIndex) => (prevIndex - 1 + expressions.length) % expressions.length);
};

const handleNextHair = () => {
setSelectedHairIndex((prevIndex) => (prevIndex + 1) % hairstyles.length);
};

const handlePreviousHair = () => {
setSelectedHairIndex((prevIndex) => (prevIndex - 1 + hairstyles.length) % hairstyles.length);
};

const handleNextBackground = () => {
setSelectedBackgroundIndex((prevIndex) => (prevIndex + 1) % backgrounds.length);
};

const handlePreviousBackground = () => {
setSelectedBackgroundIndex((prevIndex) => (prevIndex - 1 + backgrounds.length) % backgrounds.length);
};

const handleSaveAvatar = () => {
const canvas = canvasRef.current;

canvas.toBlob((blob) => {
if (blob) {
const file = new File([blob], "avatar.png", { type: "image/png" });
handleAvatarChange(file);
}
});
};

return (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-zinc-700 p-6 rounded-lg shadow-lg max-w-md w-full">
<h3 className="text-lg font-bold mb-4 text-center">Customize Your Avatar</h3>

{/* Avatar preview */}
<div className="flex justify-center items-center mb-4">
{/* Left arrow for face */}
<button onClick={handlePreviousFace} className="text-white text-2xl p-2">
<FaArrowLeft />
</button>

{/* Canvas element to draw the selected face, hairstyle, and background */}
<canvas ref={canvasRef} width="128" height="128" className="mx-4 border rounded-full"></canvas>

{/* Right arrow for face */}
<button onClick={handleNextFace} className="text-white text-2xl p-2">
<FaArrowRight />
</button>
</div>

{/* Hairstyle controls */}
<div className="flex justify-center items-center mb-4">
<button onClick={handlePreviousHair} className="text-white text-2xl p-2">
<FaArrowLeft />
</button>
<span className="text-white mx-2">Choose Hairstyle</span>
<button onClick={handleNextHair} className="text-white text-2xl p-2">
<FaArrowRight />
</button>
</div>

{/* Background controls */}
<div className="flex justify-center items-center mb-4">
<button onClick={handlePreviousBackground} className="text-white text-2xl p-2">
<FaArrowLeft />
</button>
<span className="text-white mx-2">Choose Background</span>
<button onClick={handleNextBackground} className="text-white text-2xl p-2">
<FaArrowRight />
</button>
</div>

{/* Save button */}
<div className="flex justify-end">
<button
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
onClick={handleSaveAvatar}
>
Save
</button>
</div>

{/* Cancel button */}
<div className="flex justify-end mt-2">
<button
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
onClick={() => setShowAvatarModal(false)}
>
Cancel
</button>
</div>
</div>
</div>
);
};

export default AvatarModal;
Binary file added public/images/face1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/face2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/frown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/greenbg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/grr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/long.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/orangebg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/pinkbg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/short.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/smile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/smirk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/spiky.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.