Skip to content

Commit

Permalink
Merge pull request #54 from Valik3201/feature/avatar-upload-edit
Browse files Browse the repository at this point in the history
Feature/avatar upload edit
  • Loading branch information
Valik3201 authored Jun 22, 2024
2 parents b402865 + 9266107 commit 0fa710f
Show file tree
Hide file tree
Showing 20 changed files with 685 additions and 66 deletions.
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18",
"react-easy-crop": "^5.0.7",
"react-redux": "^9.1.2",
"tailwind-datepicker-react": "^1.4.3",
"yup": "^1.4.0"
Expand Down
4 changes: 2 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { Metadata } from "next";
import { League_Spartan } from "next/font/google";
import { cookies } from "next/headers";
import dynamic from "next/dynamic";
import StoreProvider from "../store/StoreProvider";
import AuthProvider from "../store/AuthProvider";
import StoreProvider from "../providers/StoreProvider";
import AuthProvider from "../providers/AuthProvider";
import Navigation from "../components/Navbar/Navigation";
import "./globals.css";
import Footer from "../components/Footer/Footer";
Expand Down
7 changes: 5 additions & 2 deletions src/app/profile/[user]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import { useAppSelector, useAppStore } from "@/src/lib/hooks";
import { resetErrors } from "@/src/lib/features/auth/authSlice";
import AvatarUpload from "@/src/components/ProfileForm/AvatarUpload";
import AvatarUploadAndCrop from "@/src/components/ProfileForm/AvatarUploadAndCrop";
import DisplayNameForm from "@/src/components/ProfileForm/DisplayNameForm";
import EmailForm from "@/src/components/ProfileForm/EmailForm";
import SecurityForm from "@/src/components/ProfileForm/SecurityForm";
import ImageCropProvider from "@/src/providers/ImageCropProvider";

export default function Page() {
const router = useRouter();
Expand Down Expand Up @@ -39,7 +40,9 @@ export default function Page() {
</p>
</div>

<AvatarUpload />
<ImageCropProvider>
<AvatarUploadAndCrop />
</ImageCropProvider>
</div>

<DisplayNameForm />
Expand Down
31 changes: 31 additions & 0 deletions src/components/Cropper/Cropper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";
import EasyCropper from "react-easy-crop";
import { useImageCropContext } from "@/src/providers/ImageCropProvider";

const Cropper: React.FC = () => {
const { image, zoom, setZoom, rotation, crop, setCrop, onCropComplete } =
useImageCropContext();

return (
<EasyCropper
image={image || undefined}
crop={crop}
zoom={zoom}
rotation={rotation}
cropShape="round"
aspect={1}
onCropChange={(point) => setCrop((prev) => ({ ...prev, ...point }))}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
showGrid={false}
cropSize={{ width: 220, height: 220 }}
style={{
containerStyle: {
borderRadius: 8,
},
}}
/>
);
};

export default Cropper;
79 changes: 79 additions & 0 deletions src/components/Cropper/Sliders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useImageCropContext } from "@/src/providers/ImageCropProvider";
import AddIcon from "@/src/icons/AddIcon";
import RemoveIcon from "@/src/icons/RemoveIcon";
import RotateLeftIcon from "@/src/icons/RotateLeftIcon";
import RotateRightIcon from "@/src/icons/RotateRightIcon";

export const ZoomSlider = () => {
const {
zoom,
setZoom,
handleZoomIn,
handleZoomOut,
max_zoom,
min_zoom,
zoom_step,
} = useImageCropContext();

return (
<div className="flex items-center justify-center gap-2">
<button className="p-1" onClick={handleZoomOut}>
<span className="sr-only">Zoom Out</span>
<RemoveIcon />
</button>
<input
type="range"
name="volju"
min={min_zoom}
max={max_zoom}
step={zoom_step}
value={zoom}
onChange={(e) => {
setZoom(Number(e.target.value));
}}
className="accent-primary bg-gray-light h-2 rounded-full appearance-none dark:bg-light"
/>
<button className="p-1" onClick={handleZoomIn}>
<span className="sr-only">Zoom In</span>
<AddIcon />
</button>
</div>
);
};

export const RotationSlider = () => {
const {
rotation,
setRotation,
max_rotation,
min_rotation,
rotation_step,
handleRotateAntiCw,
handleRotateCw,
} = useImageCropContext();

return (
<div className="flex items-center justify-center gap-2">
<button className="p-1" onClick={handleRotateAntiCw}>
<span className="sr-only">Rotate Left</span>
<RotateLeftIcon />
</button>
<input
type="range"
name="volju"
min={min_rotation}
max={max_rotation}
step={rotation_step}
value={rotation}
onChange={(e) => {
setRotation(Number(e.target.value));
}}
className="accent-primary bg-gray-light h-2 rounded-full appearance-none dark:bg-light"
/>
<button className="p-1" onClick={handleRotateCw}>
<span className="sr-only">Rotate Right</span>
<RotateRightIcon />
</button>
</div>
);
};
12 changes: 11 additions & 1 deletion src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
"use client";

import Image from "next/image";
import { usePathname } from "next/navigation";

export default function Footer() {
const pathname = usePathname();
const isInvoicePage = pathname.startsWith("/invoices/");

return (
<footer className="mt-auto w-[327px] md:w-[672px] lg:w-[730px] mx-auto flex flex-col gap-4 pb-8 mb-20 md:mb-0">
<footer
className={`mt-auto w-[327px] md:w-[672px] lg:w-[730px] mx-auto flex flex-col gap-4 pb-8 ${
isInvoicePage ? "mb-20 md:mb-0" : ""
}`}
>
<div className="h-px w-full bg-gray-light dark:bg-dark-medium"></div>

<div className="flex gap-2 items-center justify-center pt-4">
Expand Down
38 changes: 38 additions & 0 deletions src/components/Modal/AvatarEditModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Modal from "./Modal";
import Cropper from "../Cropper/Cropper";
import { ZoomSlider, RotationSlider } from "../Cropper/Sliders";

export default function AvatarEditModal({
isOpen,
toggleState,
handleDone,
}: {
isOpen: boolean;
toggleState: () => void;
handleDone: () => void;
}) {
return (
<Modal
handleConfirm={handleDone}
trigger={null}
isOpen={isOpen}
toggleState={toggleState}
>
<Modal.Header>Edit profile picture</Modal.Header>
<Modal.Body>
<div className="flex justify-center pt-4">
<div className="relative w-60 h-60 bg-dark rounded-lg mb-4">
<Cropper />
</div>
</div>
<ZoomSlider />
<RotationSlider />
</Modal.Body>

<div className="flex gap-2 justify-end">
<Modal.DiscardBtn>Cancel</Modal.DiscardBtn>
<Modal.ConfirmBtn variant="primary">Done & Save</Modal.ConfirmBtn>
</div>
</Modal>
);
}
26 changes: 23 additions & 3 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,22 @@ interface ModalProps {
children: ReactNode;
trigger?: ReactNode | string | null;
shouldCloseOnConfirm?: boolean;
isOpen?: boolean;
toggleState?: () => void;
}

function Modal({
handleConfirm,
children,
trigger,
shouldCloseOnConfirm = true,
isOpen: externalIsOpen,
toggleState: externalToggleState,
}: ModalProps) {
const { state: isOpen, toggleState } = useToggleState(false);
const { state: internalIsOpen, toggleState: internalToggleState } =
useToggleState(false);
const isOpen = externalIsOpen !== undefined ? externalIsOpen : internalIsOpen;
const toggleState = externalToggleState || internalToggleState;

useEffect(() => {
if (isOpen) {
Expand Down Expand Up @@ -132,11 +139,24 @@ function DiscardBtn({ children }: { children: ReactNode }) {
return <Button onClick={closeModal}>{children}</Button>;
}

function ConfirmBtn({ children }: { children: ReactNode }) {
function ConfirmBtn({
children,
variant = "red",
}: {
children: ReactNode;
variant?:
| "red"
| "primary"
| "default"
| "white"
| "dark"
| "icon"
| "facebook";
}) {
const { handleConfirm } = useContext(ModalContext);

return (
<Button variant="red" type="submit" onClick={handleConfirm}>
<Button variant={variant} type="submit" onClick={handleConfirm}>
{children}
</Button>
);
Expand Down
30 changes: 30 additions & 0 deletions src/components/ProfileForm/AvatarDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Image from "next/image";
import Avatar from "@/src/icons/Avatar";

export default function AvatarDisplay({
photoURL,
}: {
photoURL: string | null;
}) {
return (
<div className="relative w-14 h-14 lg:w-20 lg:h-20">
{photoURL ? (
<Image
src={photoURL}
alt="User avatar"
className="rounded-full object-cover"
sizes="100%"
fill
/>
) : (
<Avatar size="lg" />
)}
<label
htmlFor="avatarInput"
className="absolute w-full h-full top-0 left-0 flex items-center justify-center z-20 text-heading-s-variant text-white/0 bg-dark/0 hover:bg-dark/40 hover:text-white/100 rounded-full cursor-pointer transition duration-200 ease-in-out"
>
Upload
</label>
</div>
);
}
Loading

0 comments on commit 0fa710f

Please sign in to comment.